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')}

)}
); }