
Some checks failed
GBCI / build (push) Failing after 4m39s
- Added FileBrowser component for displaying files and directories. - Introduced FileOperations component for handling file uploads and folder creation. - Created FileTree component to visualize the directory structure. - Developed DriveScreen page to integrate file browsing, operations, and UI controls. - Enhanced file system data structure for realistic representation. - Implemented search, filter, and sort functionalities in the file browser. - Added keyboard shortcuts and improved accessibility features.
585 lines
No EOL
21 KiB
TypeScript
585 lines
No EOL
21 KiB
TypeScript
"use client";
|
|
import React, { useState, useEffect, useMemo } from 'react';
|
|
import { Search, Plus, Upload, Download, Trash2, Copy, Move, Share, Star, Grid, List, Filter, SortAsc, Eye, Edit3, Archive, Clock, Users, Lock, Folder, File, Image, Video, Music, FileText, Code, Database, Package } from 'lucide-react';
|
|
|
|
// Enhanced file system with realistic data structure
|
|
const fileSystemData = {
|
|
"": {
|
|
name: "Root",
|
|
path: "",
|
|
is_dir: true,
|
|
children: ["projects", "documents", "media", "shared", "archives", "templates"]
|
|
},
|
|
"projects": {
|
|
name: "Projects",
|
|
path: "projects",
|
|
is_dir: true,
|
|
size: null,
|
|
modified: "2025-01-15T10:30:00Z",
|
|
created: "2024-12-01T09:00:00Z",
|
|
starred: true,
|
|
shared: false,
|
|
tags: ["work", "development"],
|
|
children: ["web-apps", "mobile-apps", "data-science", "ai-research", "blockchain"]
|
|
},
|
|
"projects/web-apps": {
|
|
name: "Web Applications",
|
|
path: "projects/web-apps",
|
|
is_dir: true,
|
|
size: null,
|
|
modified: "2025-01-14T16:45:00Z",
|
|
created: "2024-12-01T09:15:00Z",
|
|
starred: false,
|
|
shared: true,
|
|
tags: ["frontend", "backend", "fullstack"],
|
|
children: ["dashboard-pro", "e-commerce-platform", "social-media-app", "file-manager"]
|
|
},
|
|
"projects/web-apps/dashboard-pro": {
|
|
name: "Dashboard Pro",
|
|
path: "projects/web-apps/dashboard-pro",
|
|
is_dir: true,
|
|
size: null,
|
|
modified: "2025-01-13T14:20:00Z",
|
|
created: "2024-12-10T11:00:00Z",
|
|
starred: true,
|
|
shared: true,
|
|
tags: ["react", "typescript", "tailwind"],
|
|
children: ["src", "components", "styles", "tests", "package.json", "README.md", "deployment.yaml"]
|
|
},
|
|
"projects/web-apps/dashboard-pro/package.json": {
|
|
name: "package.json",
|
|
path: "projects/web-apps/dashboard-pro/package.json",
|
|
is_dir: false,
|
|
size: 2048,
|
|
type: "json",
|
|
modified: "2025-01-13T14:20:00Z",
|
|
created: "2024-12-10T11:00:00Z",
|
|
starred: false,
|
|
shared: false,
|
|
tags: ["config", "dependencies"]
|
|
},
|
|
"projects/web-apps/dashboard-pro/README.md": {
|
|
name: "README.md",
|
|
path: "projects/web-apps/dashboard-pro/README.md",
|
|
is_dir: false,
|
|
size: 5120,
|
|
type: "markdown",
|
|
modified: "2025-01-12T09:30:00Z",
|
|
created: "2024-12-10T11:05:00Z",
|
|
starred: false,
|
|
shared: true,
|
|
tags: ["documentation"]
|
|
},
|
|
"documents": {
|
|
name: "Documents",
|
|
path: "documents",
|
|
is_dir: true,
|
|
size: null,
|
|
modified: "2025-01-14T12:00:00Z",
|
|
created: "2024-11-01T08:00:00Z",
|
|
starred: false,
|
|
shared: false,
|
|
tags: ["personal", "work"],
|
|
children: ["proposals", "contracts", "presentations", "reports", "spreadsheets"]
|
|
},
|
|
"documents/proposals": {
|
|
name: "Proposals",
|
|
path: "documents/proposals",
|
|
is_dir: true,
|
|
size: null,
|
|
modified: "2025-01-10T15:30:00Z",
|
|
created: "2024-11-15T10:00:00Z",
|
|
starred: true,
|
|
shared: true,
|
|
tags: ["business", "client"],
|
|
children: ["Q1-2025-Strategy.pdf", "AI-Integration-Proposal.docx", "Budget-Proposal-2025.xlsx"]
|
|
},
|
|
"documents/proposals/Q1-2025-Strategy.pdf": {
|
|
name: "Q1 2025 Strategy.pdf",
|
|
path: "documents/proposals/Q1-2025-Strategy.pdf",
|
|
is_dir: false,
|
|
size: 1048576,
|
|
type: "pdf",
|
|
modified: "2025-01-10T15:30:00Z",
|
|
created: "2025-01-08T14:00:00Z",
|
|
starred: true,
|
|
shared: true,
|
|
tags: ["strategy", "quarterly", "important"]
|
|
},
|
|
"media": {
|
|
name: "Media",
|
|
path: "media",
|
|
is_dir: true,
|
|
size: null,
|
|
modified: "2025-01-13T18:45:00Z",
|
|
created: "2024-10-01T12:00:00Z",
|
|
starred: false,
|
|
shared: false,
|
|
tags: ["photos", "videos", "audio"],
|
|
children: ["photos", "videos", "audio", "graphics"]
|
|
},
|
|
"media/photos": {
|
|
name: "Photos",
|
|
path: "media/photos",
|
|
is_dir: true,
|
|
size: null,
|
|
modified: "2025-01-13T18:45:00Z",
|
|
created: "2024-10-01T12:15:00Z",
|
|
starred: false,
|
|
shared: false,
|
|
tags: ["photography"],
|
|
children: ["vacation-2024.jpg", "team-photo.jpg", "product-shots.jpg", "nature-landscape.jpg"]
|
|
},
|
|
"media/photos/vacation-2024.jpg": {
|
|
name: "vacation-2024.jpg",
|
|
path: "media/photos/vacation-2024.jpg",
|
|
is_dir: false,
|
|
size: 3145728,
|
|
type: "image",
|
|
modified: "2024-12-25T20:00:00Z",
|
|
created: "2024-12-25T20:00:00Z",
|
|
starred: true,
|
|
shared: false,
|
|
tags: ["vacation", "memories"]
|
|
},
|
|
"shared": {
|
|
name: "Shared",
|
|
path: "shared",
|
|
is_dir: true,
|
|
size: null,
|
|
modified: "2025-01-12T11:20:00Z",
|
|
created: "2024-11-01T08:30:00Z",
|
|
starred: false,
|
|
shared: true,
|
|
tags: ["collaboration", "team"],
|
|
children: ["team-resources", "client-deliverables", "public-assets"]
|
|
}
|
|
};
|
|
|
|
const getFileIcon = (item) => {
|
|
if (item.is_dir) {
|
|
return item.starred ? <Star className="w-4 h-4 text-primary" /> : <Folder className="w-4 h-4 text-accent" />;
|
|
}
|
|
|
|
const iconMap = {
|
|
pdf: <FileText className="w-4 h-4 text-red-500" />,
|
|
docx: <FileText className="w-4 h-4 text-blue-500" />,
|
|
xlsx: <Database className="w-4 h-4 text-green-500" />,
|
|
json: <Code className="w-4 h-4 text-yellow-500" />,
|
|
markdown: <Edit3 className="w-4 h-4 text-purple-500" />,
|
|
md: <Edit3 className="w-4 h-4 text-purple-500" />,
|
|
jpg: <Image className="w-4 h-4 text-pink-500" />,
|
|
jpeg: <Image className="w-4 h-4 text-pink-500" />,
|
|
png: <Image className="w-4 h-4 text-pink-500" />,
|
|
mp4: <Video className="w-4 h-4 text-red-600" />,
|
|
mp3: <Music className="w-4 h-4 text-green-600" />,
|
|
zip: <Archive className="w-4 h-4 text-orange-500" />,
|
|
yaml: <Code className="w-4 h-4 text-blue-400" />
|
|
};
|
|
|
|
return iconMap[item.type] || <File className="w-4 h-4 text-muted-foreground" />;
|
|
};
|
|
|
|
const formatFileSize = (bytes) => {
|
|
if (!bytes) return '';
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
|
};
|
|
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return '';
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
};
|
|
|
|
const FileTree = ({ onSelect, selectedPath }) => {
|
|
const [expanded, setExpanded] = useState({ "": true, "projects": true });
|
|
|
|
const toggleExpand = (path) => {
|
|
setExpanded(prev => ({ ...prev, [path]: !prev[path] }));
|
|
onSelect(path);
|
|
};
|
|
|
|
const renderTreeItem = (path, level = 0) => {
|
|
const item = fileSystemData[path];
|
|
if (!item || !item.is_dir) return null;
|
|
|
|
const isExpanded = expanded[path];
|
|
const isSelected = selectedPath === path;
|
|
|
|
return (
|
|
<div key={path}>
|
|
<div
|
|
onClick={() => toggleExpand(path)}
|
|
className={`flex items-center cursor-pointer hover:bg-accent hover:text-accent-foreground p-2 rounded transition-colors
|
|
${isSelected ? 'bg-primary text-primary-foreground' : 'text-foreground'}`}
|
|
style={{ paddingLeft: `${level * 16 + 8}px` }}
|
|
>
|
|
<div className="flex items-center space-x-2 flex-1">
|
|
{getFileIcon(item)}
|
|
<span className="text-sm font-medium truncate">{item.name}</span>
|
|
{item.starred && <Star className="w-3 h-3 text-yellow-500 fill-current" />}
|
|
{item.shared && <Users className="w-3 h-3 text-blue-500" />}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{isExpanded ? '▼' : '▶'}
|
|
</div>
|
|
</div>
|
|
{isExpanded && item.children && (
|
|
<div>
|
|
{item.children.map(childPath => {
|
|
const fullPath = path ? `${path}/${childPath}` : childPath;
|
|
return renderTreeItem(fullPath, level + 1);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="w-80 border-r border-border bg-card overflow-y-auto">
|
|
<div className="p-4 border-b border-border">
|
|
<h2 className="text-lg font-semibold text-card-foreground flex items-center">
|
|
<Folder className="w-5 h-5 mr-2 text-primary" />
|
|
File Explorer
|
|
</h2>
|
|
</div>
|
|
<div className="p-2">
|
|
{renderTreeItem("")}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const FileBrowser = ({ path, viewMode, searchTerm, sortBy, filterType }) => {
|
|
const currentItem = fileSystemData[path];
|
|
|
|
const files = useMemo(() => {
|
|
if (!currentItem || !currentItem.is_dir || !currentItem.children) return [];
|
|
|
|
let items = currentItem.children.map(childName => {
|
|
const childPath = path ? `${path}/${childName}` : childName;
|
|
return fileSystemData[childPath];
|
|
}).filter(Boolean);
|
|
|
|
// Apply search filter
|
|
if (searchTerm) {
|
|
items = items.filter(item =>
|
|
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
(item.tags && item.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase())))
|
|
);
|
|
}
|
|
|
|
// Apply type filter
|
|
if (filterType && filterType !== 'all') {
|
|
items = items.filter(item => {
|
|
if (filterType === 'folders') return item.is_dir;
|
|
if (filterType === 'files') return !item.is_dir;
|
|
if (filterType === 'starred') return item.starred;
|
|
if (filterType === 'shared') return item.shared;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// Apply sorting
|
|
items.sort((a, b) => {
|
|
switch (sortBy) {
|
|
case 'name':
|
|
return a.name.localeCompare(b.name);
|
|
case 'modified':
|
|
return new Date(b.modified || 0) - new Date(a.modified || 0);
|
|
case 'size':
|
|
return (b.size || 0) - (a.size || 0);
|
|
case 'type':
|
|
return a.type?.localeCompare(b.type || '') || 0;
|
|
default:
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
return items;
|
|
}, [currentItem, searchTerm, sortBy, filterType, path]);
|
|
|
|
if (viewMode === 'grid') {
|
|
return (
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4 p-4">
|
|
{files.map((item) => (
|
|
<div key={item.path} className="group relative">
|
|
<div className="bg-card border border-border rounded-lg p-4 hover:shadow-md transition-shadow cursor-pointer hover:bg-accent hover:text-accent-foreground">
|
|
<div className="flex flex-col items-center space-y-2">
|
|
<div className="text-3xl">
|
|
{getFileIcon(item)}
|
|
</div>
|
|
<div className="text-sm font-medium text-center truncate w-full text-card-foreground group-hover:text-accent-foreground">
|
|
{item.name}
|
|
</div>
|
|
{!item.is_dir && (
|
|
<div className="text-xs text-muted-foreground">
|
|
{formatFileSize(item.size)}
|
|
</div>
|
|
)}
|
|
<div className="flex space-x-1">
|
|
{item.starred && <Star className="w-3 h-3 text-yellow-500 fill-current" />}
|
|
{item.shared && <Users className="w-3 h-3 text-blue-500" />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="overflow-hidden">
|
|
<div className="bg-muted border-b border-border p-2">
|
|
<div className="grid grid-cols-12 gap-4 text-sm font-medium text-muted-foreground">
|
|
<div className="col-span-5">Name</div>
|
|
<div className="col-span-2">Modified</div>
|
|
<div className="col-span-1">Size</div>
|
|
<div className="col-span-2">Type</div>
|
|
<div className="col-span-2">Tags</div>
|
|
</div>
|
|
</div>
|
|
<div className="divide-y divide-border">
|
|
{files.map((item) => (
|
|
<div key={item.path} className="grid grid-cols-12 gap-4 p-3 hover:bg-accent hover:text-accent-foreground cursor-pointer group">
|
|
<div className="col-span-5 flex items-center space-x-3">
|
|
{getFileIcon(item)}
|
|
<span className="font-medium truncate">{item.name}</span>
|
|
<div className="flex space-x-1">
|
|
{item.starred && <Star className="w-3 h-3 text-yellow-500 fill-current" />}
|
|
{item.shared && <Users className="w-3 h-3 text-blue-500" />}
|
|
</div>
|
|
</div>
|
|
<div className="col-span-2 text-sm text-muted-foreground flex items-center">
|
|
{formatDate(item.modified)}
|
|
</div>
|
|
<div className="col-span-1 text-sm text-muted-foreground flex items-center">
|
|
{formatFileSize(item.size)}
|
|
</div>
|
|
<div className="col-span-2 text-sm text-muted-foreground flex items-center">
|
|
{item.is_dir ? 'Folder' : item.type?.toUpperCase() || '—'}
|
|
</div>
|
|
<div className="col-span-2 flex items-center">
|
|
<div className="flex flex-wrap gap-1">
|
|
{item.tags?.slice(0, 2).map(tag => (
|
|
<span key={tag} className="text-xs bg-secondary text-secondary-foreground px-2 py-1 rounded-full">
|
|
{tag}
|
|
</span>
|
|
))}
|
|
{item.tags?.length > 2 && (
|
|
<span className="text-xs text-muted-foreground">+{item.tags.length - 2}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const FileOperations = ({ currentPath, onRefresh }) => {
|
|
const [showUploadProgress, setShowUploadProgress] = useState(false);
|
|
const [uploadProgress, setUploadProgress] = useState(0);
|
|
|
|
const handleUpload = async () => {
|
|
setShowUploadProgress(true);
|
|
// Simulate upload progress
|
|
for (let i = 0; i <= 100; i += 10) {
|
|
setUploadProgress(i);
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
}
|
|
setTimeout(() => {
|
|
setShowUploadProgress(false);
|
|
setUploadProgress(0);
|
|
onRefresh();
|
|
}, 500);
|
|
};
|
|
|
|
const createFolder = () => {
|
|
const folderName = prompt('Enter folder name:');
|
|
if (folderName) {
|
|
onRefresh();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="border-t border-border bg-card p-4">
|
|
<div className="flex flex-wrap gap-2">
|
|
<button
|
|
onClick={handleUpload}
|
|
className="flex items-center space-x-2 bg-primary text-primary-foreground px-4 py-2 rounded-md hover:opacity-90 transition-opacity"
|
|
>
|
|
<Upload className="w-4 h-4" />
|
|
<span>Upload Files</span>
|
|
</button>
|
|
<button
|
|
onClick={createFolder}
|
|
className="flex items-center space-x-2 bg-secondary text-secondary-foreground px-4 py-2 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>New Folder</span>
|
|
</button>
|
|
<button className="flex items-center space-x-2 bg-secondary text-secondary-foreground px-4 py-2 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors">
|
|
<Download className="w-4 h-4" />
|
|
<span>Download</span>
|
|
</button>
|
|
<button className="flex items-center space-x-2 bg-secondary text-secondary-foreground px-4 py-2 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors">
|
|
<Share className="w-4 h-4" />
|
|
<span>Share</span>
|
|
</button>
|
|
<button className="flex items-center space-x-2 bg-destructive text-destructive-foreground px-4 py-2 rounded-md hover:opacity-90 transition-opacity">
|
|
<Trash2 className="w-4 h-4" />
|
|
<span>Delete</span>
|
|
</button>
|
|
</div>
|
|
|
|
{showUploadProgress && (
|
|
<div className="mt-4">
|
|
<div className="flex justify-between text-sm text-muted-foreground mb-1">
|
|
<span>Uploading files...</span>
|
|
<span>{uploadProgress}%</span>
|
|
</div>
|
|
<div className="w-full bg-secondary rounded-full h-2">
|
|
<div
|
|
className="bg-primary h-2 rounded-full transition-all duration-300"
|
|
style={{ width: `${uploadProgress}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default function DriveScreen() {
|
|
const [currentPath, setCurrentPath] = useState('');
|
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
const [viewMode, setViewMode] = useState('list');
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [sortBy, setSortBy] = useState('name');
|
|
const [filterType, setFilterType] = useState('all');
|
|
|
|
const handleRefresh = () => {
|
|
setRefreshKey(prev => prev + 1);
|
|
};
|
|
|
|
const currentItem = fileSystemData[currentPath];
|
|
const breadcrumbs = currentPath ? currentPath.split('/') : [];
|
|
|
|
return (
|
|
<div className="flex h-screen bg-background text-foreground">
|
|
<FileTree onSelect={setCurrentPath} selectedPath={currentPath} />
|
|
|
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
{/* Header */}
|
|
<div className="border-b border-border bg-card p-4">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center space-x-2">
|
|
<h1 className="text-2xl font-bold text-card-foreground">
|
|
{currentItem?.name || 'Root'}
|
|
</h1>
|
|
{currentItem?.starred && <Star className="w-5 h-5 text-yellow-500 fill-current" />}
|
|
{currentItem?.shared && <Users className="w-5 h-5 text-blue-500" />}
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<button
|
|
onClick={() => setViewMode(viewMode === 'list' ? 'grid' : 'list')}
|
|
className="p-2 hover:bg-accent hover:text-accent-foreground rounded-md transition-colors"
|
|
>
|
|
{viewMode === 'list' ? <Grid className="w-4 h-4" /> : <List className="w-4 h-4" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Breadcrumbs */}
|
|
<div className="flex items-center space-x-2 text-sm text-muted-foreground mb-4">
|
|
<button
|
|
onClick={() => setCurrentPath('')}
|
|
className="hover:text-foreground transition-colors"
|
|
>
|
|
Home
|
|
</button>
|
|
{breadcrumbs.map((crumb, index) => {
|
|
const path = breadcrumbs.slice(0, index + 1).join('/');
|
|
return (
|
|
<React.Fragment key={path}>
|
|
<span>/</span>
|
|
<button
|
|
onClick={() => setCurrentPath(path)}
|
|
className="hover:text-foreground transition-colors"
|
|
>
|
|
{crumb}
|
|
</button>
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Search and Filters */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search files and folders..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10 pr-4 py-2 w-64 bg-input border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-ring text-foreground placeholder:text-muted-foreground"
|
|
/>
|
|
</div>
|
|
|
|
<select
|
|
value={filterType}
|
|
onChange={(e) => setFilterType(e.target.value)}
|
|
className="px-3 py-2 bg-input border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-ring text-foreground"
|
|
>
|
|
<option value="all">All Items</option>
|
|
<option value="folders">Folders</option>
|
|
<option value="files">Files</option>
|
|
<option value="starred">Starred</option>
|
|
<option value="shared">Shared</option>
|
|
</select>
|
|
|
|
<select
|
|
value={sortBy}
|
|
onChange={(e) => setSortBy(e.target.value)}
|
|
className="px-3 py-2 bg-input border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-ring text-foreground"
|
|
>
|
|
<option value="name">Sort by Name</option>
|
|
<option value="modified">Sort by Modified</option>
|
|
<option value="size">Sort by Size</option>
|
|
<option value="type">Sort by Type</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* File Browser */}
|
|
<div className="flex-1 overflow-y-auto bg-background">
|
|
<FileBrowser
|
|
key={`${currentPath}-${refreshKey}`}
|
|
path={currentPath}
|
|
viewMode={viewMode}
|
|
searchTerm={searchTerm}
|
|
sortBy={sortBy}
|
|
filterType={filterType}
|
|
/>
|
|
</div>
|
|
|
|
{/* File Operations */}
|
|
<FileOperations currentPath={currentPath} onRefresh={handleRefresh} />
|
|
</div>
|
|
</div>
|
|
);
|
|
} |