import React, { useEffect, useState } from 'react' import { Folder, FileText, ChevronRight, Trash2, Pencil, RefreshCw, FolderPlus, Save, X } from 'lucide-react' import { api } from '../api/client' interface FileEntry { name: string; type: 'file' | 'directory'; size: number; mtime: string; path: string } const fmtSize = (b: number) => b > 1024 * 1024 ? `${(b / 1024 / 1024).toFixed(1)} MB` : b > 1024 ? `${(b / 1024).toFixed(0)} KB` : `${b} B` export default function FileManager() { const [path, setPath] = useState('/') const [files, setFiles] = useState([]) const [loading, setLoading] = useState(false) const [selected, setSelected] = useState>(new Set()) const [editing, setEditing] = useState<{ path: string; content: string } | null>(null) const [saving, setSaving] = useState(false) const [newDir, setNewDir] = useState('') const [showNewDir, setShowNewDir] = useState(false) const loadDir = async (p: string) => { setLoading(true); setSelected(new Set()) try { const r = await api.get(`/nixclient/files?path=${encodeURIComponent(p)}`) setFiles(r.data.data.files); setPath(r.data.data.path) } catch (e: unknown) { alert((e as { response?: { data?: { error?: string } } })?.response?.data?.error ?? 'Failed to load directory') } finally { setLoading(false) } } useEffect(() => { loadDir('/') }, []) const navigate = (entry: FileEntry) => { if (entry.type === 'directory') loadDir(entry.path) else openFile(entry.path) } const openFile = async (filePath: string) => { try { const r = await api.get(`/nixclient/files/content?path=${encodeURIComponent(filePath)}`) setEditing({ path: filePath, content: r.data.data.content }) } catch (e: unknown) { alert((e as { response?: { data?: { error?: string } } })?.response?.data?.error ?? 'Cannot open file') } } const saveFile = async () => { if (!editing) return setSaving(true) try { await api.put('/nixclient/files/content', { path: editing.path, content: editing.content }) setEditing(null) } catch (e: unknown) { alert((e as { response?: { data?: { error?: string } } })?.response?.data?.error ?? 'Save failed') } finally { setSaving(false) } } const deleteSelected = async () => { if (!selected.size || !confirm(`Delete ${selected.size} item(s)?`)) return try { await api.delete('/nixclient/files', { data: { paths: Array.from(selected) } }) loadDir(path) } catch {} } const createDir = async (e: React.FormEvent) => { e.preventDefault() try { await api.post('/nixclient/files/mkdir', { path: `${path}/${newDir}` }) setNewDir(''); setShowNewDir(false); loadDir(path) } catch (e: unknown) { alert((e as { response?: { data?: { error?: string } } })?.response?.data?.error ?? 'Failed') } } // Breadcrumb const parts = path.split('/').filter(Boolean) return (

File Manager

{selected.size > 0 && ( )}
{/* Breadcrumb */}
{parts.map((part, i) => ( ))}
{/* New dir form */} {showNewDir && (
setNewDir(e.target.value)} placeholder="folder-name" autoFocus required className="bg-[#111827] border border-[#1e2d45] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-sky-500" />
)} {/* File list */}
{['Name', 'Size', 'Modified', ''].map(h => )} {path !== '/' && ( loadDir(path.split('/').slice(0, -1).join('/') || '/')}> )} {files.map(f => ( ))} {!loading && files.length === 0 && }
{h}
..
e.stopPropagation()}> { const next = new Set(selected) e.target.checked ? next.add(f.path) : next.delete(f.path) setSelected(next) }} className="accent-sky-500" /> navigate(f)}> {f.type === 'directory' ? : } {f.name} {f.type === 'file' ? fmtSize(f.size) : '—'} {new Date(f.mtime).toLocaleDateString()} {f.type === 'file' && ( )}
Empty directory
{/* File editor modal */} {editing && (
{editing.path}