import React, { useState, useEffect, useRef } from "react"; import { FileManagerLeftSidebar } from "@/ui/Desktop/Apps/File Manager/FileManagerLeftSidebar.tsx"; import { FileManagerHomeView } from "@/ui/Desktop/Apps/File Manager/FileManagerHomeView.tsx"; import { FileManagerFileEditor } from "@/ui/Desktop/Apps/File Manager/FileManagerFileEditor.tsx"; import { FileManagerOperations } from "@/ui/Desktop/Apps/File Manager/FileManagerOperations.tsx"; import { Button } from "@/components/ui/button.tsx"; import { FIleManagerTopNavbar } from "@/ui/Desktop/Apps/File Manager/FIleManagerTopNavbar.tsx"; import { cn } from "@/lib/utils.ts"; import { Save, RefreshCw, Settings, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import { getFileManagerRecent, getFileManagerPinned, getFileManagerShortcuts, addFileManagerRecent, removeFileManagerRecent, addFileManagerPinned, removeFileManagerPinned, addFileManagerShortcut, removeFileManagerShortcut, readSSHFile, writeSSHFile, getSSHStatus, connectSSH, } from "@/ui/main-axios.ts"; import type { SSHHost, Tab } from "../../../types/index.js"; export function FileManager({ onSelectView, initialHost = null, onClose, }: { onSelectView?: (view: string) => void; embedded?: boolean; initialHost?: SSHHost | null; onClose?: () => void; }): React.ReactElement { const { t } = useTranslation(); const [tabs, setTabs] = useState([]); const [activeTab, setActiveTab] = useState("home"); const [recent, setRecent] = useState([]); const [pinned, setPinned] = useState([]); const [shortcuts, setShortcuts] = useState([]); const [currentHost, setCurrentHost] = useState(null); const [isSaving, setIsSaving] = useState(false); const [showOperations, setShowOperations] = useState(false); const [currentPath, setCurrentPath] = useState("/"); const [deletingItem, setDeletingItem] = useState(null); const sidebarRef = useRef(null); useEffect(() => { if (initialHost && (!currentHost || currentHost.id !== initialHost.id)) { setCurrentHost(initialHost); setTimeout(() => { try { const path = initialHost.defaultPath || "/"; if (sidebarRef.current && sidebarRef.current.openFolder) { sidebarRef.current.openFolder(initialHost, path); } } catch (e) {} }, 0); } }, [initialHost]); useEffect(() => { if (currentHost) { fetchHomeData(); } else { setRecent([]); setPinned([]); setShortcuts([]); } }, [currentHost]); useEffect(() => { if (activeTab === "home" && currentHost) { const interval = setInterval(() => { fetchHomeData(); }, 2000); return () => clearInterval(interval); } }, [activeTab, currentHost]); async function fetchHomeData() { if (!currentHost) return; try { const homeDataPromise = Promise.all([ getFileManagerRecent(currentHost.id), getFileManagerPinned(currentHost.id), getFileManagerShortcuts(currentHost.id), ]); const timeoutPromise = new Promise((_, reject) => setTimeout( () => reject(new Error(t("fileManager.fetchHomeDataTimeout"))), 15000, ), ); const [recentRes, pinnedRes, shortcutsRes] = (await Promise.race([ homeDataPromise, timeoutPromise, ])) as [any, any, any]; const recentWithPinnedStatus = (recentRes || []).map((file) => ({ ...file, type: "file", isPinned: (pinnedRes || []).some( (pinnedFile) => pinnedFile.path === file.path && pinnedFile.name === file.name, ), })); const pinnedWithType = (pinnedRes || []).map((file) => ({ ...file, type: "file", })); setRecent(recentWithPinnedStatus); setPinned(pinnedWithType); setShortcuts( (shortcutsRes || []).map((shortcut) => ({ ...shortcut, type: "directory", })), ); } catch (err: any) { const { toast } = await import("sonner"); toast.error(t("fileManager.failedToFetchHomeData")); if (onClose) { onClose(); } } } const formatErrorMessage = (err: any, defaultMessage: string): string => { if (typeof err === "object" && err !== null && "response" in err) { const axiosErr = err as any; if (axiosErr.response?.status === 403) { return `${t("fileManager.permissionDenied")}. ${defaultMessage}. ${t("fileManager.checkDockerLogs")}.`; } else if (axiosErr.response?.status === 500) { const backendError = axiosErr.response?.data?.error || t("fileManager.internalServerError"); return `${t("fileManager.serverError")} (500): ${backendError}. ${t("fileManager.checkDockerLogs")}.`; } else if (axiosErr.response?.data?.error) { const backendError = axiosErr.response.data.error; return `${axiosErr.response?.status ? `${t("fileManager.error")} ${axiosErr.response.status}: ` : ""}${backendError}. ${t("fileManager.checkDockerLogs")}.`; } else { return `${t("fileManager.requestFailed")} ${axiosErr.response?.status || t("fileManager.unknown")}. ${t("fileManager.checkDockerLogs")}.`; } } else if (err instanceof Error) { return `${err.message}. ${t("fileManager.checkDockerLogs")}.`; } else { return `${defaultMessage}. ${t("fileManager.checkDockerLogs")}.`; } }; const handleOpenFile = async (file: any) => { const tabId = file.path; if (!tabs.find((t) => t.id === tabId)) { const currentSshSessionId = currentHost?.id.toString(); setTabs([ ...tabs, { id: tabId, title: file.name, fileName: file.name, content: "", filePath: file.path, isSSH: true, sshSessionId: currentSshSessionId, loading: true, }, ]); try { const res = await readSSHFile(currentSshSessionId, file.path); setTabs((tabs) => tabs.map((t) => t.id === tabId ? { ...t, content: res.content, loading: false, error: undefined, } : t, ), ); await addFileManagerRecent({ name: file.name, path: file.path, isSSH: true, sshSessionId: currentSshSessionId, hostId: currentHost?.id, }); } catch (err: any) { const errorMessage = formatErrorMessage( err, t("fileManager.cannotReadFile"), ); toast.error(errorMessage); setTabs((tabs) => tabs.map((t) => (t.id === tabId ? { ...t, loading: false } : t)), ); } } setActiveTab(tabId); }; const handleRemoveRecent = async (file: any) => { try { await removeFileManagerRecent({ name: file.name, path: file.path, isSSH: true, sshSessionId: file.sshSessionId, hostId: currentHost?.id, }); fetchHomeData(); } catch (err) {} }; const handlePinFile = async (file: any) => { try { await addFileManagerPinned({ name: file.name, path: file.path, isSSH: true, sshSessionId: file.sshSessionId, hostId: currentHost?.id, }); if (sidebarRef.current && sidebarRef.current.fetchFiles) { sidebarRef.current.fetchFiles(); } } catch (err) {} }; const handleUnpinFile = async (file: any) => { try { await removeFileManagerPinned({ name: file.name, path: file.path, isSSH: true, sshSessionId: file.sshSessionId, hostId: currentHost?.id, }); if (sidebarRef.current && sidebarRef.current.fetchFiles) { sidebarRef.current.fetchFiles(); } } catch (err) {} }; const handleOpenShortcut = async (shortcut: any) => { if (sidebarRef.current?.isOpeningShortcut) { return; } if (sidebarRef.current && sidebarRef.current.openFolder) { try { sidebarRef.current.isOpeningShortcut = true; const normalizedPath = shortcut.path.startsWith("/") ? shortcut.path : `/${shortcut.path}`; await sidebarRef.current.openFolder(currentHost, normalizedPath); } catch (err) { } finally { if (sidebarRef.current) { sidebarRef.current.isOpeningShortcut = false; } } } else { } }; const handleAddShortcut = async (folderPath: string) => { try { const name = folderPath.split("/").pop() || folderPath; await addFileManagerShortcut({ name, path: folderPath, isSSH: true, sshSessionId: currentHost?.id.toString(), hostId: currentHost?.id, }); } catch (err) {} }; const handleRemoveShortcut = async (shortcut: any) => { try { await removeFileManagerShortcut({ name: shortcut.name, path: shortcut.path, isSSH: true, sshSessionId: currentHost?.id.toString(), hostId: currentHost?.id, }); } catch (err) {} }; const closeTab = (tabId: string | number) => { const idx = tabs.findIndex((t) => t.id === tabId); const newTabs = tabs.filter((t) => t.id !== tabId); setTabs(newTabs); if (activeTab === tabId) { if (newTabs.length > 0) setActiveTab(newTabs[Math.max(0, idx - 1)].id); else setActiveTab("home"); } }; const setTabContent = (tabId: string | number, content: string) => { setTabs((tabs) => tabs.map((t) => t.id === tabId ? { ...t, content, dirty: true, error: undefined, success: undefined, } : t, ), ); }; const handleSave = async (tab: Tab) => { if (isSaving) { return; } setIsSaving(true); try { if (!tab.sshSessionId) { throw new Error(t("fileManager.noSshSessionId")); } if (!tab.filePath) { throw new Error(t("fileManager.noFilePath")); } if (!currentHost?.id) { throw new Error(t("fileManager.noCurrentHost")); } try { const statusPromise = getSSHStatus(tab.sshSessionId); const statusTimeoutPromise = new Promise((_, reject) => setTimeout( () => reject(new Error(t("fileManager.sshStatusCheckTimeout"))), 10000, ), ); const status = (await Promise.race([ statusPromise, statusTimeoutPromise, ])) as { connected: boolean }; if (!status.connected) { const connectPromise = connectSSH(tab.sshSessionId, { hostId: currentHost.id, ip: currentHost.ip, port: currentHost.port, username: currentHost.username, password: currentHost.password, sshKey: currentHost.key, keyPassword: currentHost.keyPassword, authType: currentHost.authType, credentialId: currentHost.credentialId, userId: currentHost.userId, }); const connectTimeoutPromise = new Promise((_, reject) => setTimeout( () => reject(new Error(t("fileManager.sshReconnectionTimeout"))), 15000, ), ); await Promise.race([connectPromise, connectTimeoutPromise]); } } catch (statusErr) {} const savePromise = writeSSHFile( tab.sshSessionId, tab.filePath, tab.content, ); const timeoutPromise = new Promise((_, reject) => setTimeout(() => { reject(new Error(t("fileManager.saveOperationTimeout"))); }, 30000), ); const result = await Promise.race([savePromise, timeoutPromise]); setTabs((tabs) => tabs.map((t) => t.id === tab.id ? { ...t, loading: false, } : t, ), ); if (result?.toast) { toast[result.toast.type](result.toast.message); } else { toast.success(t("fileManager.fileSavedSuccessfully")); } Promise.allSettled([ (async () => { try { await addFileManagerRecent({ name: tab.fileName, path: tab.filePath, isSSH: true, sshSessionId: tab.sshSessionId, hostId: currentHost.id, }); } catch (recentErr) {} })(), ]).then(() => {}); } catch (err) { let errorMessage = formatErrorMessage( err, t("fileManager.cannotSaveFile"), ); if ( errorMessage.includes("timed out") || errorMessage.includes("timeout") ) { errorMessage = t("fileManager.saveTimeout"); } toast.error(`${t("fileManager.failedToSaveFile")}: ${errorMessage}`); setTabs((tabs) => tabs.map((t) => t.id === tab.id ? { ...t, loading: false, } : t, ), ); } finally { setIsSaving(false); } }; const handleHostChange = (_host: SSHHost | null) => {}; const handleOperationComplete = () => { if (sidebarRef.current && sidebarRef.current.fetchFiles) { sidebarRef.current.fetchFiles(); } }; const handleSuccess = (message: string) => { toast.success(message); }; const handleError = (error: string) => { toast.error(error); }; const updateCurrentPath = (newPath: string) => { setCurrentPath(newPath); }; const handleDeleteFromSidebar = (item: any) => { setDeletingItem(item); }; const performDelete = async (item: any) => { if (!currentHost?.id) return; try { const { deleteSSHItem } = await import("@/ui/main-axios.ts"); const response = await deleteSSHItem( currentHost.id.toString(), item.path, item.type === "directory", ); if (response?.toast) { toast[response.toast.type](response.toast.message); } else { toast.success( `${item.type === "directory" ? t("fileManager.folder") : t("fileManager.file")} ${t("fileManager.deletedSuccessfully")}`, ); } setDeletingItem(null); handleOperationComplete(); } catch (error: any) { handleError( error?.response?.data?.error || t("fileManager.failedToDeleteItem"), ); } }; if (!currentHost) { return (
{})} onOpenFile={handleOpenFile} tabs={tabs} ref={sidebarRef} host={initialHost as SSHHost} onOperationComplete={handleOperationComplete} onError={handleError} onSuccess={handleSuccess} onPathChange={updateCurrentPath} />

{t("fileManager.connectToServer")}

{t("fileManager.selectServerToEdit")}

); } return (
{})} onOpenFile={handleOpenFile} tabs={tabs} ref={sidebarRef} host={currentHost as SSHHost} onOperationComplete={handleOperationComplete} onError={handleError} onSuccess={handleSuccess} onPathChange={updateCurrentPath} onDeleteItem={handleDeleteFromSidebar} />
({ id: t.id, title: t.title }))} activeTab={activeTab} setActiveTab={setActiveTab} closeTab={closeTab} onHomeClick={() => { setActiveTab("home"); }} />
{activeTab === "home" ? ( ) : ( (() => { const tab = tabs.find((t) => t.id === activeTab); if (!tab) return null; return (
setTabContent(tab.id, content) } />
); })() )}
{showOperations && (
)}
{deletingItem && (

{t("fileManager.confirmDelete")}

{t("fileManager.confirmDeleteMessage", { name: deletingItem.name, })} {deletingItem.type === "directory" && ` ${t("fileManager.deleteDirectoryWarning")}`}

{t("fileManager.actionCannotBeUndone")}

)}
); }