* Add documentation in Chinese language (#160) * Update file naming and structure for mobile support * Add conditional desktop/mobile rendering * Mobile terminal * Fix overwritten i18n (#161) * Add comprehensive Chinese internationalization support - Implemented i18n framework with react-i18next for multi-language support - Added Chinese (zh) and English (en) translation files with comprehensive coverage - Localized Admin interface, authentication flows, and error messages - Translated FileManager operations and UI elements - Updated HomepageAuth component with localized authentication messages - Localized LeftSidebar navigation and host management - Added language switcher component (shown after login only) - Configured default language as English with Chinese as secondary option - Localized TOTPSetup two-factor authentication interface - Updated Docker build to include translation files - Achieved 95%+ UI localization coverage across core components Co-Authored-By: Claude <noreply@anthropic.com> * Extend Chinese localization coverage to Host Manager components - Added comprehensive translations for HostManagerHostViewer component - Localized all host management UI text including import/export features - Translated error messages and confirmation dialogs for host operations - Added translations for HostManagerHostEditor validation messages - Localized connection details, organization settings, and form labels - Fixed syntax error in FileManagerOperations component - Achieved near-complete localization of SSH host management interface - Updated placeholders and tooltips for better user guidance Co-Authored-By: Claude <noreply@anthropic.com> * Complete comprehensive Chinese localization for Termix - Added full localization support for Tunnel components (connected/disconnected states, retry messages) - Localized all tunnel status messages and connection errors - Added translations for port forwarding UI elements - Verified Server, TopNavbar, and Tab components already have complete i18n support - Achieved 99%+ localization coverage across entire application - All core UI components now fully support Chinese and English languages This completes the comprehensive internationalization effort for the Termix SSH management platform. Co-Authored-By: Claude <noreply@anthropic.com> * Localize additional Host Manager components and authentication settings - Added translations for all authentication options (Password, Key, SSH Private Key) - Localized form labels in HostManagerHostEditor (Pin Connection, Enable Terminal/Tunnel/FileManager) - Translated Upload/Update Key button states - Localized Host Viewer and Add/Edit Host tab labels - Added Chinese translations for all host management settings - Fixed duplicate translation keys in JSON files Co-Authored-By: Claude <noreply@anthropic.com> * Extend localization coverage to UI components and common strings - Added comprehensive common translations (online/offline, success/error, etc.) - Localized status indicator component with all status states - Updated FileManagerLeftSidebar toast messages for rename/delete operations - Added translations for UI elements (close, toggle sidebar, etc.) - Expanded placeholder translations for form inputs - Added Chinese translations for all new common strings - Improved consistency across component status messages Co-Authored-By: Claude <noreply@anthropic.com> * Complete Chinese localization for remaining UI components - Add comprehensive Chinese translations for Host Manager component - Translate all form labels, buttons, and descriptions - Add translations for SSH configuration warnings and instructions - Localize tunnel connection settings and port forwarding options - Localize SSH Tools panel - Translate key recording functionality - Add translations for settings and configuration options - Translate homepage welcome messages and navigation elements - Add Chinese translations for login success messages - Localize "Updates & Releases" section title - Translate sidebar "Host Manager" button - Fix translation key display issues - Remove duplicate translation keys in both language files - Ensure all components properly reference translation keys - Fix hosts.tunnelConnections key mapping This completes the full Chinese localization of the Termix application, achieving near 100% UI translation coverage while maintaining English as the default language. * Complete final Chinese localization for Host Manager tunnel configuration - Add Chinese translations for authentication UI elements - Translate "Authentication", "Password", and "Key" tab labels - Localize SSH private key and key password fields - Add translations for key type selector - Localize tunnel connection configuration descriptions - Translate retry attempts and retry interval descriptions - Add dynamic tunnel forwarding description with port parameters - Localize endpoint SSH configuration labels - Fix missing translation keys - Add "upload" translation for file upload button - Ensure all FormLabel and FormDescription elements use translation keys This completes the comprehensive Chinese localization of the entire Termix application, achieving 100% UI translation coverage. * Fix PR feedback: Improve Profile section translations and UX - Fixed password reset translations in Profile section - Moved language selector from TopNavbar to Profile page - Added profile.selectPreferredLanguage translation key - Improved user experience for language preferences * Apply critical OIDC and notification system fixes while preserving i18n - Merge OIDC authentication fixes from3877e90: * Enhanced JWKS discovery mechanism with multiple backup URLs * Better support for non-standard OIDC providers (Authentik, etc.) * Improved error handling for "Failed to get user information" - Migrate to unified Sonner toast notification system: * Replace custom success/error state management * Remove redundant alert state variables * Consistent user feedback across all components - Improve code quality and function naming conventions - PRESERVE all existing i18n functionality and Chinese translations 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * Fix OIDC errors for "Failed to get user information" * Fix OIDC errors for "Failed to get user information" * Fix spelling error * Migrate everything to alert system, update user.ts for OIDC updates. * Fix OIDC errors for "Failed to get user information" * Fix OIDC errors for "Failed to get user information" * Fix spelling error * Migrate everything to alert system, update user.ts for OIDC updates. * Update env * Fix users.ts and schema for override * Convert web app to Electron desktop application - Add Electron main process with developer tools support - Create preload script for secure context bridge - Configure electron-builder for packaging - Update Vite config for Electron compatibility (base: './') - Add environment variable support for API host configuration - Fix i18n to use relative paths for Electron file protocol - Restore multi-port backend architecture (8081-8085) - Add enhanced backend startup script with port checking - Update package.json with Electron dependencies and build scripts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Complete Electron desktop application implementation - Add backend auto-start functionality in main process - Fix authentication token storage for Electron environment - Implement localStorage-based token management in Electron - Add proper Electron environment detection via preload script - Fix WebSocket connections for terminal functionality - Resolve font file loading issues in packaged application - Update API endpoints to work with backend auto-start - Streamline build scripts with unified electron:package command - Fix better-sqlite3 native module compatibility issues - Ensure all services start automatically in production mode 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Remove releases folder from git and force Desktop UI. * Improve mobile support with half-baked custom keyboard * Fix API routing * Upgrade mobile keyboard with more keys. * Add cross-platform support and clean up obsolete files - Add electron-packager scripts for Windows, macOS, and Linux - Include universal architecture support for macOS - Add electron:package:all for building all platforms - Remove obsolete start-backend.sh script (replaced by Electron auto-start) - Improve ignore patterns to exclude repo-images folder - Add platform-specific icon configurations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix build system by removing electron-builder dependency - Remove electron-builder and @electron/rebuild packages to resolve build errors - Clean up package.json scripts that depend on electron-builder - Fix merge conflict markers in AdminSettings.tsx and PasswordReset.tsx - All build commands now work correctly: - npm run build (frontend + backend) - npm run build:frontend - npm run build:backend - npm run electron:package (using electron-packager) The build system is now stable and functional without signing requirements. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> * Mobile UI improvement * Electron dev (#185) * Add comprehensive Chinese internationalization support - Implemented i18n framework with react-i18next for multi-language support - Added Chinese (zh) and English (en) translation files with comprehensive coverage - Localized Admin interface, authentication flows, and error messages - Translated FileManager operations and UI elements - Updated HomepageAuth component with localized authentication messages - Localized LeftSidebar navigation and host management - Added language switcher component (shown after login only) - Configured default language as English with Chinese as secondary option - Localized TOTPSetup two-factor authentication interface - Updated Docker build to include translation files - Achieved 95%+ UI localization coverage across core components Co-Authored-By: Claude <noreply@anthropic.com> * Extend Chinese localization coverage to Host Manager components - Added comprehensive translations for HostManagerHostViewer component - Localized all host management UI text including import/export features - Translated error messages and confirmation dialogs for host operations - Added translations for HostManagerHostEditor validation messages - Localized connection details, organization settings, and form labels - Fixed syntax error in FileManagerOperations component - Achieved near-complete localization of SSH host management interface - Updated placeholders and tooltips for better user guidance Co-Authored-By: Claude <noreply@anthropic.com> * Complete comprehensive Chinese localization for Termix - Added full localization support for Tunnel components (connected/disconnected states, retry messages) - Localized all tunnel status messages and connection errors - Added translations for port forwarding UI elements - Verified Server, TopNavbar, and Tab components already have complete i18n support - Achieved 99%+ localization coverage across entire application - All core UI components now fully support Chinese and English languages This completes the comprehensive internationalization effort for the Termix SSH management platform. Co-Authored-By: Claude <noreply@anthropic.com> * Localize additional Host Manager components and authentication settings - Added translations for all authentication options (Password, Key, SSH Private Key) - Localized form labels in HostManagerHostEditor (Pin Connection, Enable Terminal/Tunnel/FileManager) - Translated Upload/Update Key button states - Localized Host Viewer and Add/Edit Host tab labels - Added Chinese translations for all host management settings - Fixed duplicate translation keys in JSON files Co-Authored-By: Claude <noreply@anthropic.com> * Extend localization coverage to UI components and common strings - Added comprehensive common translations (online/offline, success/error, etc.) - Localized status indicator component with all status states - Updated FileManagerLeftSidebar toast messages for rename/delete operations - Added translations for UI elements (close, toggle sidebar, etc.) - Expanded placeholder translations for form inputs - Added Chinese translations for all new common strings - Improved consistency across component status messages Co-Authored-By: Claude <noreply@anthropic.com> * Complete Chinese localization for remaining UI components - Add comprehensive Chinese translations for Host Manager component - Translate all form labels, buttons, and descriptions - Add translations for SSH configuration warnings and instructions - Localize tunnel connection settings and port forwarding options - Localize SSH Tools panel - Translate key recording functionality - Add translations for settings and configuration options - Translate homepage welcome messages and navigation elements - Add Chinese translations for login success messages - Localize "Updates & Releases" section title - Translate sidebar "Host Manager" button - Fix translation key display issues - Remove duplicate translation keys in both language files - Ensure all components properly reference translation keys - Fix hosts.tunnelConnections key mapping This completes the full Chinese localization of the Termix application, achieving near 100% UI translation coverage while maintaining English as the default language. * Complete final Chinese localization for Host Manager tunnel configuration - Add Chinese translations for authentication UI elements - Translate "Authentication", "Password", and "Key" tab labels - Localize SSH private key and key password fields - Add translations for key type selector - Localize tunnel connection configuration descriptions - Translate retry attempts and retry interval descriptions - Add dynamic tunnel forwarding description with port parameters - Localize endpoint SSH configuration labels - Fix missing translation keys - Add "upload" translation for file upload button - Ensure all FormLabel and FormDescription elements use translation keys This completes the comprehensive Chinese localization of the entire Termix application, achieving 100% UI translation coverage. * Fix PR feedback: Improve Profile section translations and UX - Fixed password reset translations in Profile section - Moved language selector from TopNavbar to Profile page - Added profile.selectPreferredLanguage translation key - Improved user experience for language preferences * Apply critical OIDC and notification system fixes while preserving i18n - Merge OIDC authentication fixes from3877e90: * Enhanced JWKS discovery mechanism with multiple backup URLs * Better support for non-standard OIDC providers (Authentik, etc.) * Improved error handling for "Failed to get user information" - Migrate to unified Sonner toast notification system: * Replace custom success/error state management * Remove redundant alert state variables * Consistent user feedback across all components - Improve code quality and function naming conventions - PRESERVE all existing i18n functionality and Chinese translations 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * Fix OIDC errors for "Failed to get user information" * Fix OIDC errors for "Failed to get user information" * Fix spelling error * Migrate everything to alert system, update user.ts for OIDC updates. * Fix OIDC errors for "Failed to get user information" * Fix OIDC errors for "Failed to get user information" * Fix spelling error * Migrate everything to alert system, update user.ts for OIDC updates. * Update env * Fix users.ts and schema for override * Convert web app to Electron desktop application - Add Electron main process with developer tools support - Create preload script for secure context bridge - Configure electron-builder for packaging - Update Vite config for Electron compatibility (base: './') - Add environment variable support for API host configuration - Fix i18n to use relative paths for Electron file protocol - Restore multi-port backend architecture (8081-8085) - Add enhanced backend startup script with port checking - Update package.json with Electron dependencies and build scripts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Complete Electron desktop application implementation - Add backend auto-start functionality in main process - Fix authentication token storage for Electron environment - Implement localStorage-based token management in Electron - Add proper Electron environment detection via preload script - Fix WebSocket connections for terminal functionality - Resolve font file loading issues in packaged application - Update API endpoints to work with backend auto-start - Streamline build scripts with unified electron:package command - Fix better-sqlite3 native module compatibility issues - Ensure all services start automatically in production mode 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Remove releases folder from git and force Desktop UI. * Improve mobile support with half-baked custom keyboard * Fix API routing * Upgrade mobile keyboard with more keys. * Add cross-platform support and clean up obsolete files - Add electron-packager scripts for Windows, macOS, and Linux - Include universal architecture support for macOS - Add electron:package:all for building all platforms - Remove obsolete start-backend.sh script (replaced by Electron auto-start) - Improve ignore patterns to exclude repo-images folder - Add platform-specific icon configurations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix build system by removing electron-builder dependency - Remove electron-builder and @electron/rebuild packages to resolve build errors - Clean up package.json scripts that depend on electron-builder - Fix merge conflict markers in AdminSettings.tsx and PasswordReset.tsx - All build commands now work correctly: - npm run build (frontend + backend) - npm run build:frontend - npm run build:backend - npm run electron:package (using electron-packager) The build system is now stable and functional without signing requirements. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: Karmaa <88517757+LukeGus@users.noreply.github.com> * Add navigation and hardcoded hosts * Update mobile sidebar to use API, add auth and tab system to mobile. * Update sidebar state * Mobile support (#190) * Add vibration to keyboard * Fix keyboard keys * Fix keyboard keys * Fix keyboard keys * Rename files, improve keyboard usability * Improve keyboard view and fix various issues with it * Add mobile chinese translation * Disable OS keyboard from appearing * Fix fit addon not resizing with "more" on keyboard * Disable OS keyboard on terminal load * Merge Luke and Zac * feat: add export option for ssh hosts (#173) (#187) * Update issue templates * feat: add export JSON option for SSH hosts (#173) --------- Co-authored-by: Karmaa <88517757+LukeGus@users.noreply.github.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> * feat(profile): display version number from .env in profile menu (#182) * feat(profile): display version number from .env in profile menu * Update version checking process --------- Co-authored-by: LukeGus <bugattiguy527@gmail.com> * Add pretier * feat(auth): Add password visibility toggle to auth forms (#166) * added hide and unhide password button * Undo admin settings changes --------- Co-authored-by: LukeGus <bugattiguy527@gmail.com> * Re-added password input * Remove encrpytion, improve logging and merge interfaces. * Improve logging (backend and frontend) and added dedicde OIDC clear * feat: Added option to paste private key (#203) * Improve logging frontend/backend, fix host form being reversed. * Improve logging more, fix credentials sync issues, migrate more to be toasts * Improve logging more, fix credentials sync issues, migrate more to be toasts * More error to toast migration * Remove more inline styles and run npm updates * Update homepage appearing over everything and terminal incorrect bg * Improved server stat generation and UI by caching and supporting more platforms * Update mobile app with the same stat changes and remove rate limiting * Put user profle in its own tab, add code rabbit support * Improve code rabbit yaml * Update chinese translation and fix z indexs causing delay to hide * Bump vite from 7.1.3 to 7.1.5 (#204) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.3 to 7.1.5. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.1.5/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 7.1.5 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update read me * Update electron builder and fix mobile terminal background * Update logo, move translations, update electron building. * Remove backend from electon, switching to server manager * Add electron server configurator * Fix backend builder on Dockerfile * Fix langauge file for Dockerfile * Fix architecture issues in Dockerfile * Fix architecture issues in Dockerfile * Fix architecture issues in Dockerfile * Fix backend building for docker image * Add electron builder * Fix node starting in entrypoint and remove release from electron build * Remove double packaing in electron build * Fix folder nesting for electron gbuilder * Fix native module docker build (better-sql and bcrypt) * Fix api routes and missing translations and improve reconnection for terminals * Update read me for new installation method * Update CONTRIBUTING.md with color scheme * Fix terrminal not closing afer 3 tries * Fix electronm api routing, fikx ssh not connecting, and OIDC redirect errors * Fix more electron API issues (ssh/oidc), make server manager force API check, and login saving. * Add electron API routes * Fix more electron APi routes and issues * Hide admin settings on electron and fix server manager URl verification * Hide admin settings on electron and fix server manager URl verification * Fix admin setting visiblity on electron * Add links to docs in respective places * Migrate all getCookies to use main-axios. * Migrate all isElectron to use main-axios. * Clean up backend files * Clean up frontend files and read me translations * Run prettier * Fix terminal in web, and update translations and prep for release. * Update API to work on devs and remove random letter * Run prettier * Update read me for release * Update read me for release * Fixed delete issue (ready for release) * Ensure retention days for artifact upload are set --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: starry <115192496+sky22333@users.noreply.github.com> Co-authored-by: ZacharyZcR <PayasoNorahC@protonmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> Co-authored-by: Shivam Kumar <155747305+maishivamhoo123@users.noreply.github.com> Co-authored-by: Abhilash Gandhamalla <150357125+AbhilashG12@users.noreply.github.com> Co-authored-by: jedi04 <78037206+jedi04@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
631 lines
22 KiB
TypeScript
631 lines
22 KiB
TypeScript
import React, {
|
|
useEffect,
|
|
useState,
|
|
useRef,
|
|
forwardRef,
|
|
useImperativeHandle,
|
|
} from "react";
|
|
import {
|
|
Folder,
|
|
File,
|
|
ArrowUp,
|
|
Pin,
|
|
MoreVertical,
|
|
Trash2,
|
|
Edit3,
|
|
} from "lucide-react";
|
|
import { ScrollArea } from "@/components/ui/scroll-area.tsx";
|
|
import { cn } from "@/lib/utils.ts";
|
|
import { Input } from "@/components/ui/input.tsx";
|
|
import { Button } from "@/components/ui/button.tsx";
|
|
import { toast } from "sonner";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
listSSHFiles,
|
|
renameSSHItem,
|
|
deleteSSHItem,
|
|
getFileManagerPinned,
|
|
addFileManagerPinned,
|
|
removeFileManagerPinned,
|
|
getSSHStatus,
|
|
connectSSH,
|
|
} from "@/ui/main-axios.ts";
|
|
import type { SSHHost } from "../../../types/index.js";
|
|
|
|
const FileManagerLeftSidebar = forwardRef(function FileManagerSidebar(
|
|
{
|
|
onOpenFile,
|
|
tabs,
|
|
host,
|
|
onOperationComplete,
|
|
onPathChange,
|
|
onDeleteItem,
|
|
}: {
|
|
onSelectView?: (view: string) => void;
|
|
onOpenFile: (file: any) => void;
|
|
tabs: any[];
|
|
host: SSHHost;
|
|
onOperationComplete?: () => void;
|
|
onError?: (error: string) => void;
|
|
onSuccess?: (message: string) => void;
|
|
onPathChange?: (path: string) => void;
|
|
onDeleteItem?: (item: any) => void;
|
|
},
|
|
ref,
|
|
) {
|
|
const { t } = useTranslation();
|
|
const [currentPath, setCurrentPath] = useState("/");
|
|
const [files, setFiles] = useState<any[]>([]);
|
|
const pathInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const [search, setSearch] = useState("");
|
|
const [debouncedSearch, setDebouncedSearch] = useState("");
|
|
const [fileSearch, setFileSearch] = useState("");
|
|
const [debouncedFileSearch, setDebouncedFileSearch] = useState("");
|
|
useEffect(() => {
|
|
const handler = setTimeout(() => setDebouncedSearch(search), 200);
|
|
return () => clearTimeout(handler);
|
|
}, [search]);
|
|
useEffect(() => {
|
|
const handler = setTimeout(() => setDebouncedSearch(fileSearch), 200);
|
|
return () => clearTimeout(handler);
|
|
}, [fileSearch]);
|
|
|
|
const [sshSessionId, setSshSessionId] = useState<string | null>(null);
|
|
const [filesLoading, setFilesLoading] = useState(false);
|
|
const [connectingSSH, setConnectingSSH] = useState(false);
|
|
const [connectionCache, setConnectionCache] = useState<
|
|
Record<
|
|
string,
|
|
{
|
|
sessionId: string;
|
|
timestamp: number;
|
|
}
|
|
>
|
|
>({});
|
|
const [fetchingFiles, setFetchingFiles] = useState(false);
|
|
|
|
const [contextMenu, setContextMenu] = useState<{
|
|
visible: boolean;
|
|
x: number;
|
|
y: number;
|
|
item: any;
|
|
}>({
|
|
visible: false,
|
|
x: 0,
|
|
y: 0,
|
|
item: null,
|
|
});
|
|
|
|
const [renamingItem, setRenamingItem] = useState<{
|
|
item: any;
|
|
newName: string;
|
|
} | null>(null);
|
|
|
|
useEffect(() => {
|
|
const nextPath = host?.defaultPath || "/";
|
|
setCurrentPath(nextPath);
|
|
onPathChange?.(nextPath);
|
|
(async () => {
|
|
await connectToSSH(host);
|
|
})();
|
|
}, [host?.id]);
|
|
|
|
async function connectToSSH(server: SSHHost): Promise<string | null> {
|
|
const sessionId = server.id.toString();
|
|
|
|
const cached = connectionCache[sessionId];
|
|
if (cached && Date.now() - cached.timestamp < 30000) {
|
|
setSshSessionId(cached.sessionId);
|
|
return cached.sessionId;
|
|
}
|
|
|
|
if (connectingSSH) {
|
|
return null;
|
|
}
|
|
|
|
setConnectingSSH(true);
|
|
|
|
try {
|
|
if (!server.password && !server.key) {
|
|
toast.error(t("common.noAuthCredentials"));
|
|
return null;
|
|
}
|
|
|
|
const connectionConfig = {
|
|
hostId: server.id,
|
|
ip: server.ip,
|
|
port: server.port,
|
|
username: server.username,
|
|
password: server.password,
|
|
sshKey: server.key,
|
|
keyPassword: server.keyPassword,
|
|
authType: server.authType,
|
|
credentialId: server.credentialId,
|
|
userId: server.userId,
|
|
};
|
|
|
|
await connectSSH(sessionId, connectionConfig);
|
|
|
|
setSshSessionId(sessionId);
|
|
|
|
setConnectionCache((prev) => ({
|
|
...prev,
|
|
[sessionId]: { sessionId, timestamp: Date.now() },
|
|
}));
|
|
|
|
return sessionId;
|
|
} catch (err: any) {
|
|
toast.error(
|
|
err?.response?.data?.error || t("fileManager.failedToConnectSSH"),
|
|
);
|
|
setSshSessionId(null);
|
|
return null;
|
|
} finally {
|
|
setConnectingSSH(false);
|
|
}
|
|
}
|
|
|
|
async function fetchFiles() {
|
|
if (fetchingFiles) {
|
|
return;
|
|
}
|
|
|
|
setFetchingFiles(true);
|
|
setFiles([]);
|
|
setFilesLoading(true);
|
|
|
|
try {
|
|
let pinnedFiles: any[] = [];
|
|
try {
|
|
if (host) {
|
|
pinnedFiles = await getFileManagerPinned(host.id);
|
|
}
|
|
} catch (err) {}
|
|
|
|
if (host && sshSessionId) {
|
|
let res: any[] = [];
|
|
|
|
try {
|
|
const status = await getSSHStatus(sshSessionId);
|
|
if (!status.connected) {
|
|
const newSessionId = await connectToSSH(host);
|
|
if (newSessionId) {
|
|
setSshSessionId(newSessionId);
|
|
res = await listSSHFiles(newSessionId, currentPath);
|
|
} else {
|
|
throw new Error(t("fileManager.failedToReconnectSSH"));
|
|
}
|
|
} else {
|
|
res = await listSSHFiles(sshSessionId, currentPath);
|
|
}
|
|
} catch (sessionErr) {
|
|
const newSessionId = await connectToSSH(host);
|
|
if (newSessionId) {
|
|
setSshSessionId(newSessionId);
|
|
res = await listSSHFiles(newSessionId, currentPath);
|
|
} else {
|
|
throw sessionErr;
|
|
}
|
|
}
|
|
|
|
const processedFiles = (res || []).map((f: any) => {
|
|
const filePath =
|
|
currentPath + (currentPath.endsWith("/") ? "" : "/") + f.name;
|
|
const isPinned = pinnedFiles.some(
|
|
(pinned) => pinned.path === filePath,
|
|
);
|
|
return {
|
|
...f,
|
|
path: filePath,
|
|
isPinned,
|
|
isSSH: true,
|
|
sshSessionId: sshSessionId,
|
|
};
|
|
});
|
|
|
|
setFiles(processedFiles);
|
|
}
|
|
} catch (err: any) {
|
|
setFiles([]);
|
|
toast.error(
|
|
err?.response?.data?.error ||
|
|
err?.message ||
|
|
t("fileManager.failedToListFiles"),
|
|
);
|
|
} finally {
|
|
setFilesLoading(false);
|
|
setFetchingFiles(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (host && sshSessionId && !connectingSSH && !fetchingFiles) {
|
|
const timeoutId = setTimeout(() => {
|
|
fetchFiles();
|
|
}, 100);
|
|
return () => clearTimeout(timeoutId);
|
|
}
|
|
}, [currentPath, host, sshSessionId]);
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
openFolder: async (_server: SSHHost, path: string) => {
|
|
if (connectingSSH || fetchingFiles) {
|
|
return;
|
|
}
|
|
|
|
if (currentPath === path) {
|
|
setTimeout(() => fetchFiles(), 100);
|
|
return;
|
|
}
|
|
|
|
setFetchingFiles(false);
|
|
setFilesLoading(false);
|
|
setFiles([]);
|
|
|
|
setCurrentPath(path);
|
|
onPathChange?.(path);
|
|
if (!sshSessionId) {
|
|
const sessionId = await connectToSSH(host);
|
|
if (sessionId) setSshSessionId(sessionId);
|
|
}
|
|
},
|
|
fetchFiles: () => {
|
|
if (host && sshSessionId) {
|
|
fetchFiles();
|
|
}
|
|
},
|
|
getCurrentPath: () => currentPath,
|
|
}));
|
|
|
|
useEffect(() => {
|
|
if (pathInputRef.current) {
|
|
pathInputRef.current.scrollLeft = pathInputRef.current.scrollWidth;
|
|
}
|
|
}, [currentPath]);
|
|
|
|
const filteredFiles = files.filter((file) => {
|
|
const q = debouncedFileSearch.trim().toLowerCase();
|
|
if (!q) return true;
|
|
return file.name.toLowerCase().includes(q);
|
|
});
|
|
|
|
const handleContextMenu = (e: React.MouseEvent, item: any) => {
|
|
e.preventDefault();
|
|
|
|
const viewportWidth = window.innerWidth;
|
|
const viewportHeight = window.innerHeight;
|
|
|
|
const menuWidth = 160;
|
|
const menuHeight = 80;
|
|
|
|
let x = e.clientX;
|
|
let y = e.clientY;
|
|
|
|
if (x + menuWidth > viewportWidth) {
|
|
x = e.clientX - menuWidth;
|
|
}
|
|
|
|
if (y + menuHeight > viewportHeight) {
|
|
y = e.clientY - menuHeight;
|
|
}
|
|
|
|
if (x < 0) {
|
|
x = 0;
|
|
}
|
|
|
|
if (y < 0) {
|
|
y = 0;
|
|
}
|
|
|
|
setContextMenu({
|
|
visible: true,
|
|
x,
|
|
y,
|
|
item,
|
|
});
|
|
};
|
|
|
|
const closeContextMenu = () => {
|
|
setContextMenu({ visible: false, x: 0, y: 0, item: null });
|
|
};
|
|
|
|
const handleRename = async (item: any, newName: string) => {
|
|
if (!sshSessionId || !newName.trim() || newName === item.name) {
|
|
setRenamingItem(null);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await renameSSHItem(sshSessionId, item.path, newName.trim());
|
|
toast.success(
|
|
`${item.type === "directory" ? t("common.folder") : t("common.file")} ${t("common.renamedSuccessfully")}`,
|
|
);
|
|
setRenamingItem(null);
|
|
if (onOperationComplete) {
|
|
onOperationComplete();
|
|
} else {
|
|
fetchFiles();
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(
|
|
error?.response?.data?.error || t("fileManager.failedToRenameItem"),
|
|
);
|
|
}
|
|
};
|
|
|
|
const startRename = (item: any) => {
|
|
setRenamingItem({ item, newName: item.name });
|
|
closeContextMenu();
|
|
};
|
|
|
|
const startDelete = (item: any) => {
|
|
onDeleteItem?.(item);
|
|
closeContextMenu();
|
|
};
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = () => closeContextMenu();
|
|
document.addEventListener("click", handleClickOutside);
|
|
return () => document.removeEventListener("click", handleClickOutside);
|
|
}, []);
|
|
|
|
const handlePathChange = (newPath: string) => {
|
|
setCurrentPath(newPath);
|
|
onPathChange?.(newPath);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col h-full w-[256px] max-w-[256px]">
|
|
<div className="flex flex-col flex-grow min-h-0">
|
|
<div className="flex-1 w-full h-full flex flex-col bg-dark-bg-darkest border-r-2 border-dark-border overflow-hidden p-0 relative min-h-0">
|
|
{host && (
|
|
<div className="flex flex-col h-full w-full max-w-[260px]">
|
|
<div className="flex items-center gap-2 px-2 py-1.5 border-b-2 border-dark-border bg-dark-bg z-20 max-w-[260px]">
|
|
<Button
|
|
size="icon"
|
|
variant="outline"
|
|
className="h-9 w-9 bg-dark-bg border-2 border-dark-border rounded-md hover:bg-dark-hover focus:outline-none focus:ring-2 focus:ring-ring"
|
|
onClick={() => {
|
|
let path = currentPath;
|
|
if (path && path !== "/" && path !== "") {
|
|
if (path.endsWith("/")) path = path.slice(0, -1);
|
|
const lastSlash = path.lastIndexOf("/");
|
|
if (lastSlash > 0) {
|
|
handlePathChange(path.slice(0, lastSlash));
|
|
} else {
|
|
handlePathChange("/");
|
|
}
|
|
} else {
|
|
handlePathChange("/");
|
|
}
|
|
}}
|
|
>
|
|
<ArrowUp className="w-4 h-4" />
|
|
</Button>
|
|
<Input
|
|
ref={pathInputRef}
|
|
value={currentPath}
|
|
onChange={(e) => handlePathChange(e.target.value)}
|
|
className="flex-1 bg-dark-bg border-2 border-dark-border-hover text-white truncate rounded-md px-2 py-1 focus:outline-none focus:ring-2 focus:ring-ring hover:border-dark-border-light"
|
|
/>
|
|
</div>
|
|
<div className="px-2 py-2 border-b-1 border-dark-border bg-dark-bg">
|
|
<Input
|
|
placeholder={t("fileManager.searchFilesAndFolders")}
|
|
className="w-full h-7 text-sm bg-dark-bg-button border-2 border-dark-border-hover text-white placeholder:text-muted-foreground rounded-md"
|
|
autoComplete="off"
|
|
value={fileSearch}
|
|
onChange={(e) => setFileSearch(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="flex-1 min-h-0 w-full bg-dark-bg-darkest border-t-1 border-dark-border">
|
|
<ScrollArea className="h-full w-full bg-dark-bg-darkest">
|
|
<div className="p-2 pb-0">
|
|
{connectingSSH || filesLoading ? (
|
|
<div className="text-xs text-muted-foreground">
|
|
{t("common.loading")}
|
|
</div>
|
|
) : filteredFiles.length === 0 ? (
|
|
<div className="text-xs text-muted-foreground">
|
|
{t("fileManager.noFilesOrFoldersFound")}
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col gap-1">
|
|
{filteredFiles.map((item: any) => {
|
|
const isOpen = (tabs || []).some(
|
|
(t: any) => t.id === item.path,
|
|
);
|
|
const isRenaming =
|
|
renamingItem?.item?.path === item.path;
|
|
const isDeleting = false;
|
|
|
|
return (
|
|
<div
|
|
key={item.path}
|
|
className={cn(
|
|
"flex items-center gap-2 px-3 py-2 bg-dark-bg border-2 border-dark-border rounded group max-w-[220px] mb-2 relative",
|
|
isOpen &&
|
|
"opacity-60 cursor-not-allowed pointer-events-none",
|
|
)}
|
|
onContextMenu={(e) =>
|
|
!isOpen && handleContextMenu(e, item)
|
|
}
|
|
>
|
|
{isRenaming ? (
|
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
{item.type === "directory" ? (
|
|
<Folder className="w-4 h-4 text-blue-400 flex-shrink-0" />
|
|
) : (
|
|
<File className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
|
)}
|
|
<Input
|
|
value={renamingItem.newName}
|
|
onChange={(e) =>
|
|
setRenamingItem((prev) =>
|
|
prev
|
|
? {
|
|
...prev,
|
|
newName: e.target.value,
|
|
}
|
|
: null,
|
|
)
|
|
}
|
|
className="flex-1 h-6 text-sm bg-dark-bg-button border border-dark-border-hover text-white"
|
|
autoFocus
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") {
|
|
handleRename(
|
|
item,
|
|
renamingItem.newName,
|
|
);
|
|
} else if (e.key === "Escape") {
|
|
setRenamingItem(null);
|
|
}
|
|
}}
|
|
onBlur={() =>
|
|
handleRename(item, renamingItem.newName)
|
|
}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div
|
|
className="flex items-center gap-2 flex-1 cursor-pointer min-w-0"
|
|
onClick={() =>
|
|
!isOpen &&
|
|
(item.type === "directory"
|
|
? handlePathChange(item.path)
|
|
: onOpenFile({
|
|
name: item.name,
|
|
path: item.path,
|
|
isSSH: item.isSSH,
|
|
sshSessionId: item.sshSessionId,
|
|
}))
|
|
}
|
|
>
|
|
{item.type === "directory" ? (
|
|
<Folder className="w-4 h-4 text-blue-400 flex-shrink-0" />
|
|
) : (
|
|
<File className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
|
)}
|
|
<span className="text-sm text-white truncate flex-1 min-w-0">
|
|
{item.name}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
{item.type === "file" && (
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
className="h-7 w-7"
|
|
disabled={isOpen}
|
|
onClick={async (e) => {
|
|
e.stopPropagation();
|
|
try {
|
|
if (item.isPinned) {
|
|
await removeFileManagerPinned({
|
|
name: item.name,
|
|
path: item.path,
|
|
hostId: host?.id,
|
|
isSSH: true,
|
|
sshSessionId:
|
|
host?.id.toString(),
|
|
});
|
|
setFiles(
|
|
files.map((f) =>
|
|
f.path === item.path
|
|
? {
|
|
...f,
|
|
isPinned: false,
|
|
}
|
|
: f,
|
|
),
|
|
);
|
|
} else {
|
|
await addFileManagerPinned({
|
|
name: item.name,
|
|
path: item.path,
|
|
hostId: host?.id,
|
|
isSSH: true,
|
|
sshSessionId:
|
|
host?.id.toString(),
|
|
});
|
|
setFiles(
|
|
files.map((f) =>
|
|
f.path === item.path
|
|
? {
|
|
...f,
|
|
isPinned: true,
|
|
}
|
|
: f,
|
|
),
|
|
);
|
|
}
|
|
} catch (err) {}
|
|
}}
|
|
>
|
|
<Pin
|
|
className={`w-1 h-1 ${item.isPinned ? "text-yellow-400 fill-current" : "text-muted-foreground"}`}
|
|
/>
|
|
</Button>
|
|
)}
|
|
{!isOpen && (
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
className="h-7 w-7 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleContextMenu(e, item);
|
|
}}
|
|
>
|
|
<MoreVertical className="w-4 h-4" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{contextMenu.visible && contextMenu.item && (
|
|
<div
|
|
className="fixed z-[99998] bg-dark-bg border-2 border-dark-border rounded-lg shadow-xl py-1 min-w-[160px]"
|
|
style={{
|
|
left: contextMenu.x,
|
|
top: contextMenu.y,
|
|
}}
|
|
>
|
|
<button
|
|
className="w-full px-3 py-2 text-left text-sm text-white hover:bg-dark-hover flex items-center gap-2"
|
|
onClick={() => startRename(contextMenu.item)}
|
|
>
|
|
<Edit3 className="w-4 h-4" />
|
|
Rename
|
|
</button>
|
|
<button
|
|
className="w-full px-3 py-2 text-left text-sm text-red-400 hover:bg-dark-hover flex items-center gap-2"
|
|
onClick={() => startDelete(contextMenu.item)}
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
Delete
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
export { FileManagerLeftSidebar };
|