mirror of
https://github.com/YuzuZensai/Termix.git
synced 2026-09-13 18:58:52 +00:00
v1.11.0 (#523)
* Feature request network graph
* Fixing PR442:
- Fixed:
- UI design elemets
- UI and button colors
- JSON export
- recent activity is default again
- Removed:
- Online/Offline UI labels
- left-click menu on hosts
- Added:
- small pulsing dot inside the hosts to indicate online status like in the left bar
* fix: electron build errors and skip macos job
* fix: testflight submit failure
* fix: made submit job match build type
* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)
* Update Crowdin configuration file
* Update Crowdin configuration file
* fix: resolve Vite build warnings for mixed static/dynamic imports
- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
- react-vendor: React and React DOM
- ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
- monaco: Monaco Editor
- codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB
This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)
* fix: build error on docker
* Handle enter button (#481)
* Update Crowdin configuration file
* Update Crowdin configuration file
* Update Linux Portable section with AUR link (#474)
* fix: file manager incorrectly decoding/encoding when editing files (#476)
* fix: electron build errors and skip macos job
* fix: testflight submit failure
* fix: made submit job match build type
* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)
* Update Crowdin configuration file
* Update Crowdin configuration file
* fix: resolve Vite build warnings for mixed static/dynamic imports
- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
- react-vendor: React and React DOM
- ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
- monaco: Monaco Editor
- codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB
This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)
---------
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* fix: build error on docker (#477)
* fix: electron build errors and skip macos job
* fix: testflight submit failure
* fix: made submit job match build type
* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)
* Update Crowdin configuration file
* Update Crowdin configuration file
* fix: resolve Vite build warnings for mixed static/dynamic imports
- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
- react-vendor: React and React DOM
- ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
- monaco: Monaco Editor
- codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB
This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)
* fix: build error on docker
---------
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* Increase max old space size for npm builds
* Increase Node.js memory limit in Dockerfile
* Remove NODE_OPTIONS from build commands in Dockerfile
* Change runner to blacksmith-4vcpu-ubuntu-2404
* fix: build error on docker
* Add handle on enter button;
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
* fix: remove top tech
* fix: update readme
* fix: prevent long container names from overflowing card (#496)
Added min-w-0 to CardTitle to allow text truncation in flexbox.
Without this, flex items have min-width: auto which prevents
the truncate class from working properly.
Fixes #411
* fix: use SFTP readdir for file listing to support non-Linux systems (#495)
The file manager now uses SFTP readdir as the primary method for
listing files, with ls -la as a fallback. This enables compatibility
with MikroTik RouterOS and other non-Linux systems that don't have
standard shell commands.
Fixes #317
* fix: restore SSH connection timeout to 120s for 2FA authentication (#494)
The timeout was reduced from 120s to 30s in v1.10, causing 2FA login
failures. Users with keyboard-interactive authentication (TOTP/2FA)
need sufficient time to enter their verification codes before the
SSH connection times out.
Fixes #404
* feat: add Docker container healthcheck (#493)
* fix: owner should not be marked as shared when host is shared to their role (#492)
* fix: use correct MIME types for image preview (#491)
* fix: prevent session reset when updating host properties (#490)
* fix: add shell creation timeout and improve error handling (#489)
* fix: set default lineHeight to 1.0 for TUI apps compatibility (#488)
* fix: delete all related data when removing user (#487)
* fix: nginx permission denied on restricted kernels (#486)
* fix: skip existing hosts and credentials during JSON import (#485)
Added duplicate detection for SSH hosts (by ip+port+username) and
credentials (by name) during import. Existing items are now skipped
by default, or updated if replaceExisting option is enabled.
This matches the existing behavior of importDismissedAlerts.
Fixes #389
* feat: add firewall status widget for server stats (#484)
* Feature: PWA (#479)
* feat: add PWA support with offline capabilities
- Add web app manifest with icons and theme configuration
- Add service worker with cache-first strategy for static assets
- Add useServiceWorker hook for SW registration
- Add PWA meta tags and Apple-specific tags to index.html
- Update vite.config.ts for optimal asset caching
* Update package-lock.json
* New Crowdin updates (#472)
* New translations en.json (Romanian)
* New translations en.json (French)
* New translations en.json (Spanish)
* New translations en.json (Afrikaans)
* New translations en.json (Arabic)
* New translations en.json (Catalan)
* New translations en.json (Czech)
* New translations en.json (Danish)
* New translations en.json (German)
* New translations en.json (Greek)
* New translations en.json (Finnish)
* New translations en.json (Hebrew)
* New translations en.json (Hungarian)
* New translations en.json (Italian)
* New translations en.json (Japanese)
* New translations en.json (Korean)
* New translations en.json (Dutch)
* New translations en.json (Norwegian)
* New translations en.json (Polish)
* New translations en.json (Portuguese)
* New translations en.json (Russian)
* New translations en.json (Serbian (Cyrillic))
* New translations en.json (Swedish)
* New translations en.json (Turkish)
* New translations en.json (Ukrainian)
* New translations en.json (Chinese Simplified)
* New translations en.json (English)
* New translations en.json (Vietnamese)
* New translations en.json (German)
* feat: add listening ports widget for server stats (#483)
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
* feat: fix network stats merge and add openapi jsdocs comments
* feat: add workflow/config to auto generate openapi json
* feat: remove locales
* feat: support URL routes to open terminal directly (#156) (#503)
* fix: resolve merge conflict artifacts in dev-1.10.1
- Fix missing closing tags in AppView.tsx NetworkGraphView
- Fix incomplete catch blocks in server-stats.ts and db/index.ts
- Fix missing closing brace in en.json ports section
- Fix HostManagerApp.tsx import path
- Fix stats-widgets.ts type definition
- Fix schema.ts networkTopology table definition
- Add type annotations in user-data-import.ts
* feat: support URL routes to open terminal directly (#156)
- Add /terminal/{hostNameOrId} route for new format
- Keep /hosts/{id}/terminal for backward compatibility
- Smart detection: numeric IDs for ID lookup, otherwise name lookup
- Clean URL after opening to prevent duplicate on refresh
- Show toast error when host not found
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
* feat: add Ctrl+Alt key remapping for browser-blocked shortcuts (#501)
Browsers intercept Ctrl+W/T/N/Q, making them unusable in terminal.
This adds Ctrl+Alt+<key> as an alternative that sends Ctrl+<key>.
- Ctrl+Alt+W → Ctrl+W (nano search, delete word)
- Ctrl+Alt+T → Ctrl+T (transpose chars)
- Ctrl+Alt+N → Ctrl+N (next line)
- Ctrl+Alt+Q → Ctrl+Q (XON flow control)
Fixes Termix-SSH/Support#407
* feat: remove locales
* New Crowdin updates (#504)
* New translations en.json (Romanian)
* New translations en.json (French)
* New translations en.json (Spanish)
* New translations en.json (Afrikaans)
* New translations en.json (Arabic)
* New translations en.json (Catalan)
* New translations en.json (Czech)
* New translations en.json (Danish)
* New translations en.json (German)
* New translations en.json (Greek)
* New translations en.json (Finnish)
* New translations en.json (Hebrew)
* New translations en.json (Hungarian)
* New translations en.json (Italian)
* New translations en.json (Japanese)
* New translations en.json (Korean)
* New translations en.json (Dutch)
* New translations en.json (Norwegian)
* New translations en.json (Polish)
* New translations en.json (Portuguese)
* New translations en.json (Russian)
* New translations en.json (Serbian (Cyrillic))
* New translations en.json (Swedish)
* New translations en.json (Turkish)
* New translations en.json (Ukrainian)
* New translations en.json (Chinese Simplified)
* New translations en.json (English)
* New translations en.json (Vietnamese)
* New translations en.json (German)
* New translations en.json (Norwegian)
* New translations en.json (Romanian)
* New translations en.json (French)
* New translations en.json (Spanish)
* New translations en.json (Arabic)
* New translations en.json (Czech)
* New translations en.json (Danish)
* New translations en.json (Greek)
* New translations en.json (Finnish)
* New translations en.json (Italian)
* New translations en.json (Japanese)
* New translations en.json (Dutch)
* New translations en.json (Norwegian)
* New translations en.json (Polish)
* New translations en.json (Portuguese)
* New translations en.json (Russian)
* New translations en.json (Swedish)
* New translations en.json (Ukrainian)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* New translations en.json (Finnish)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* feat: add option to disable update checker (#502)
* feat: add option to disable update checker
Add a new setting in User Profile > Settings to disable automatic
update checking on startup and dashboard.
- Adds 'Disable Update Check' toggle in profile settings
- Skips GitHub API calls when disabled (reduces network requests)
- Works for both web app and Electron client
Fixes Termix-SSH/Support#410
* feat: remove locales
---------
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
* New Crowdin updates (#505)
* New translations en.json (Romanian)
* New translations en.json (French)
* New translations en.json (Spanish)
* New translations en.json (Afrikaans)
* New translations en.json (Arabic)
* New translations en.json (Catalan)
* New translations en.json (Czech)
* New translations en.json (Danish)
* New translations en.json (German)
* New translations en.json (Greek)
* New translations en.json (Finnish)
* New translations en.json (Hebrew)
* New translations en.json (Hungarian)
* New translations en.json (Italian)
* New translations en.json (Japanese)
* New translations en.json (Korean)
* New translations en.json (Dutch)
* New translations en.json (Norwegian)
* New translations en.json (Polish)
* New translations en.json (Portuguese)
* New translations en.json (Russian)
* New translations en.json (Serbian (Cyrillic))
* New translations en.json (Swedish)
* New translations en.json (Turkish)
* New translations en.json (Ukrainian)
* New translations en.json (Chinese Simplified)
* New translations en.json (English)
* New translations en.json (Vietnamese)
* New translations en.json (German)
* New translations en.json (Norwegian)
* New translations en.json (Romanian)
* New translations en.json (French)
* New translations en.json (Spanish)
* New translations en.json (Arabic)
* New translations en.json (Czech)
* New translations en.json (Danish)
* New translations en.json (Greek)
* New translations en.json (Finnish)
* New translations en.json (Italian)
* New translations en.json (Japanese)
* New translations en.json (Dutch)
* New translations en.json (Norwegian)
* New translations en.json (Polish)
* New translations en.json (Portuguese)
* New translations en.json (Russian)
* New translations en.json (Swedish)
* New translations en.json (Ukrainian)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* New translations en.json (Finnish)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* New translations en.json (Bulgarian)
* New translations en.json (Indonesian)
* New translations en.json (Hindi)
* feat: add crowdin i18n
* feat: remove locales
* New Crowdin updates (#506)
* New translations en.json (Romanian)
* New translations en.json (French)
* New translations en.json (Spanish)
* New translations en.json (Afrikaans)
* New translations en.json (Arabic)
* New translations en.json (Catalan)
* New translations en.json (Czech)
* New translations en.json (Danish)
* New translations en.json (German)
* New translations en.json (Greek)
* New translations en.json (Finnish)
* New translations en.json (Hebrew)
* New translations en.json (Hungarian)
* New translations en.json (Italian)
* New translations en.json (Japanese)
* New translations en.json (Korean)
* New translations en.json (Dutch)
* New translations en.json (Norwegian)
* New translations en.json (Polish)
* New translations en.json (Portuguese)
* New translations en.json (Russian)
* New translations en.json (Serbian (Cyrillic))
* New translations en.json (Swedish)
* New translations en.json (Turkish)
* New translations en.json (Ukrainian)
* New translations en.json (Chinese Simplified)
* New translations en.json (English)
* New translations en.json (Vietnamese)
* New translations en.json (German)
* New translations en.json (Norwegian)
* New translations en.json (Romanian)
* New translations en.json (French)
* New translations en.json (Spanish)
* New translations en.json (Arabic)
* New translations en.json (Czech)
* New translations en.json (Danish)
* New translations en.json (Greek)
* New translations en.json (Finnish)
* New translations en.json (Italian)
* New translations en.json (Japanese)
* New translations en.json (Dutch)
* New translations en.json (Norwegian)
* New translations en.json (Polish)
* New translations en.json (Portuguese)
* New translations en.json (Russian)
* New translations en.json (Swedish)
* New translations en.json (Ukrainian)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* New translations en.json (Finnish)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* New translations en.json (Bulgarian)
* New translations en.json (Indonesian)
* New translations en.json (Hindi)
* New translations en.json (Romanian)
* New translations en.json (French)
* New translations en.json (Spanish)
* New translations en.json (Afrikaans)
* New translations en.json (Arabic)
* New translations en.json (Catalan)
* New translations en.json (Czech)
* New translations en.json (German)
* New translations en.json (Greek)
* New translations en.json (Finnish)
* New translations en.json (Hebrew)
* New translations en.json (Hungarian)
* New translations en.json (Italian)
* New translations en.json (Japanese)
* New translations en.json (Korean)
* New translations en.json (Dutch)
* New translations en.json (Polish)
* New translations en.json (Portuguese)
* New translations en.json (Russian)
* New translations en.json (Serbian (Cyrillic))
* New translations en.json (Turkish)
* New translations en.json (Ukrainian)
* New translations en.json (Chinese Simplified)
* New translations en.json (Chinese Traditional)
* New translations en.json (Vietnamese)
* New translations en.json (Portuguese, Brazilian)
* New translations en.json (Bulgarian)
* New translations en.json (Indonesian)
* New translations en.json (Hindi)
* New translations en.json (Bengali)
* New translations en.json (Thai)
* feat: update readme
* feat: update readme
* feat: update credential editor to use submitting system and add health monitor
* feat: added toggle for command pallete
* feat: added close button on tab dropdown
* feat: added sidebar management and improved some host manager UI/UX
* feat: re-added missing users.ts route from merge
* feat: add toggle for password reset feature in admin settings (#508)
* feat: add sudo support for file manager operations (#509)
* fix: add sudo support for listFiles and improve permission error handling (#512)
* feat: add sudo support for file manager operations
* fix: add sudo support for listFiles and improve permission error handling
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
* feat: fix sudo password dialog ui, add totp/pass reset limiting, and refreshed users screen when auth is outdated
* feat: add copy password button and fixed new line carriage issues and backend crash for auth key
* feat: added quick connection system (ad-hoc)
* Enter Key for Quick Login (#513)
* feat: added -r and -l support for tunnels
* feat: begin dashboard overhaul by splitting into cards and adding customization
* feat: improved full screen apps, overhauled dashboard, updated server stats ui, etc.
* feat: add auth.tsx suppot for fullscreen
* feat: greatly improve network graph ui/ux and migrated to use translations and theme system
* feat: update to use blacksmith
* feat: improve ui for customized tabs and hide add/edit host/credential when submiting
* feat: add warpgate support with a dialog (terminal only)
* feat: expand warpgate to docker/file manager
* fix: docker not working wtih warpgate and none auth failing for terminal
* fix: prevent owner permission loss when sharing host to own role (#514)
* Update Crowdin configuration file
* Update Crowdin configuration file
* Update Linux Portable section with AUR link (#474)
* fix: file manager incorrectly decoding/encoding when editing files (#476)
* fix: electron build errors and skip macos job
* fix: testflight submit failure
* fix: made submit job match build type
* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)
* Update Crowdin configuration file
* Update Crowdin configuration file
* fix: resolve Vite build warnings for mixed static/dynamic imports
- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
- react-vendor: React and React DOM
- ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
- monaco: Monaco Editor
- codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB
This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)
---------
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* fix: build error on docker (#477)
* fix: electron build errors and skip macos job
* fix: testflight submit failure
* fix: made submit job match build type
* fix: resolve Vite build warnings for mixed static/dynamic imports (#473)
* Update Crowdin configuration file
* Update Crowdin configuration file
* fix: resolve Vite build warnings for mixed static/dynamic imports
- Convert all dynamic imports of main-axios.ts to static imports (10 files)
- Convert all dynamic imports of sonner to static imports (4 files)
- Add manual chunking configuration to vite.config.ts for better bundle splitting
- react-vendor: React and React DOM
- ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge
- monaco: Monaco Editor
- codemirror: CodeMirror and related packages
- Increase chunkSizeWarningLimit to 1000kB
This resolves Vite warnings about mixed import strategies preventing
proper code-splitting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* fix: file manager incorrectly decoding/encoding when editing files (made base64/utf8 dependent)
* fix: build error on docker
---------
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
* Increase max old space size for npm builds
* Increase Node.js memory limit in Dockerfile
* Remove NODE_OPTIONS from build commands in Dockerfile
* Change runner to blacksmith-4vcpu-ubuntu-2404
* fix: build error on docker
* fix: prevent owner permission loss when sharing host to own role
Fixes #391
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
* fix: SSH key passphrase not passed for Docker and Tunnel (#521)
* perf: optimize Host Manager for large host lists
- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)
Fixes performance issues when managing ~1000 hosts with status monitoring enabled.
* fix: SSH key passphrase not passed to ssh2 for Docker and Tunnel
Database field is `key_password` but code used `keyPassword`.
Added fallback to check both field names.
Affected:
- docker.ts: Docker SSH connections with encrypted keys
- tunnel.ts: Tunnel connections with encrypted keys
---------
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
* perf: optimize Host Manager for large host lists (#520)
- Add ServerStatusContext for shared status polling (reduces API calls from N to 1)
- Move TooltipProvider to component root (eliminates N context instances)
- Add pagination with "Show More" button (limits initial DOM nodes per folder)
Fixes performance issues when managing ~1000 hosts with status monitoring enabled.
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
* fix: add missing mimeTypes definition for image preview (#518)
Fixes Termix-SSH/Support#408
* fix: prevent session reset when updating host properties (#517)
Move WebSocket cleanup logic to a separate unmount-only effect to
prevent SSH sessions from being closed when host properties are updated.
Closes Termix-SSH/Support#401
* fix: backend type error
* feat: make terminal connections more resilient, added connection log, and fixed https/proxy reconnection loop (issue #385)
* feat: improved conneciton log ui/logic
* feat: improved conneciton log ui/logic
* feat: expanded connection log to work across all components (readying for release)
* feat: update readme
* feat: update readme
* fix: build error
* fix: build error
* fix: changed ver
* chore: clean up
* chore: continue clean up
* fix: remove attempts remaining and fix electron errors and some connection log ui inconsistencies
* fix: added missing nginx routes and fixed sudo password copy with sudo password autofil field
* fix: update readme and run cleaner
* fix: update readme
* fix: update readme
* fix: update readme
* feat: update chinese readme
---------
Co-authored-by: Steven Josefs <s.josefs@gmx.de>
Co-authored-by: Jefferson Nunn <89030989+jeffersonwarrior@users.noreply.github.com>
Co-authored-by: Termix CI <ci@termix.dev>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com>
Co-authored-by: Gaylord Julien <g.j@mailbox.org>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: Aditya Tawade <36890395+aditya-tawade@users.noreply.github.com>
This commit is contained in:
co-authored by
Steven Josefs
Jefferson Nunn
Termix CI
Claude
Nunzio Marfè
Gaylord Julien
ZacharyZcR
Aditya Tawade
parent
366667ca92
commit
536dc35258
@@ -17,10 +17,12 @@ import { Button } from "@/components/ui/button.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TOTPDialog } from "@/ui/desktop/navigation/TOTPDialog.tsx";
|
||||
import { SSHAuthDialog } from "@/ui/desktop/navigation/SSHAuthDialog.tsx";
|
||||
import { TOTPDialog } from "@/ui/desktop/navigation/dialogs/TOTPDialog.tsx";
|
||||
import { SSHAuthDialog } from "@/ui/desktop/navigation/dialogs/SSHAuthDialog.tsx";
|
||||
import { WarpgateDialog } from "@/ui/desktop/navigation/dialogs/WarpgateDialog.tsx";
|
||||
import { PermissionsDialog } from "./components/PermissionsDialog.tsx";
|
||||
import { CompressDialog } from "./components/CompressDialog.tsx";
|
||||
import { SudoPasswordDialog } from "./SudoPasswordDialog.tsx";
|
||||
import {
|
||||
Upload,
|
||||
FolderPlus,
|
||||
@@ -32,6 +34,12 @@ import {
|
||||
} from "lucide-react";
|
||||
import { TerminalWindow } from "./components/TerminalWindow.tsx";
|
||||
import type { SSHHost, FileItem } from "../../../types/index.js";
|
||||
import {
|
||||
ConnectionLogProvider,
|
||||
useConnectionLog,
|
||||
} from "@/ui/desktop/navigation/connection-log/ConnectionLogContext.tsx";
|
||||
import { ConnectionLog } from "@/ui/desktop/navigation/connection-log/ConnectionLog.tsx";
|
||||
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
|
||||
import {
|
||||
listSSHFiles,
|
||||
uploadSSHFile,
|
||||
@@ -44,6 +52,7 @@ import {
|
||||
moveSSHItem,
|
||||
connectSSH,
|
||||
verifySSHTOTP,
|
||||
verifySSHWarpgate,
|
||||
getSSHStatus,
|
||||
keepSSHAlive,
|
||||
identifySSHSymlink,
|
||||
@@ -57,6 +66,7 @@ import {
|
||||
changeSSHPermissions,
|
||||
extractSSHArchive,
|
||||
compressSSHFiles,
|
||||
setSudoPassword,
|
||||
} from "@/ui/main-axios.ts";
|
||||
import type { SidebarItem } from "./FileManagerSidebar.tsx";
|
||||
|
||||
@@ -94,6 +104,11 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
const { openWindow } = useWindowManager();
|
||||
const { t } = useTranslation();
|
||||
const { confirmWithToast } = useConfirmation();
|
||||
const {
|
||||
addLog,
|
||||
clearLogs,
|
||||
isExpanded: isConnectionLogExpanded,
|
||||
} = useConnectionLog();
|
||||
|
||||
const [currentHost] = useState<SSHHost | null>(initialHost || null);
|
||||
const [currentPath, setCurrentPath] = useState(
|
||||
@@ -112,6 +127,12 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
const [totpRequired, setTotpRequired] = useState(false);
|
||||
const [totpSessionId, setTotpSessionId] = useState<string | null>(null);
|
||||
const [totpPrompt, setTotpPrompt] = useState<string>("");
|
||||
const [warpgateRequired, setWarpgateRequired] = useState(false);
|
||||
const [warpgateSessionId, setWarpgateSessionId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [warpgateUrl, setWarpgateUrl] = useState<string>("");
|
||||
const [warpgateSecurityKey, setWarpgateSecurityKey] = useState<string>("");
|
||||
const [showAuthDialog, setShowAuthDialog] = useState(false);
|
||||
const [authDialogReason, setAuthDialogReason] = useState<
|
||||
"no_keyboard" | "auth_failed" | "timeout"
|
||||
@@ -119,6 +140,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
const [pinnedFiles, setPinnedFiles] = useState<Set<string>>(new Set());
|
||||
const [sidebarRefreshTrigger, setSidebarRefreshTrigger] = useState(0);
|
||||
const [isClosing, setIsClosing] = useState<boolean>(false);
|
||||
const [hasConnectionError, setHasConnectionError] = useState<boolean>(false);
|
||||
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
x: number;
|
||||
@@ -163,6 +185,13 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
[],
|
||||
);
|
||||
|
||||
const [sudoDialogOpen, setSudoDialogOpen] = useState(false);
|
||||
const [pendingSudoOperation, setPendingSudoOperation] = useState<
|
||||
| { type: "delete"; files: FileItem[] }
|
||||
| { type: "navigate"; path: string }
|
||||
| null
|
||||
>(null);
|
||||
|
||||
const { selectedFiles, clearSelection, setSelection } = useFileSelection();
|
||||
|
||||
const { dragHandlers } = useDragAndDrop({
|
||||
@@ -208,14 +237,14 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
|
||||
const handleCloseWithError = useCallback(
|
||||
(errorMessage: string) => {
|
||||
if (isClosing) return;
|
||||
setIsClosing(true);
|
||||
toast.error(errorMessage);
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
setHasConnectionError(true);
|
||||
addLog({
|
||||
type: "error",
|
||||
stage: "connection",
|
||||
message: errorMessage,
|
||||
});
|
||||
},
|
||||
[isClosing, onClose],
|
||||
[addLog],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -324,6 +353,8 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
initialLoadDoneRef.current = false;
|
||||
setHasConnectionError(false);
|
||||
clearLogs();
|
||||
|
||||
const sessionId = currentHost.id.toString();
|
||||
|
||||
@@ -348,6 +379,15 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
socks5ProxyChain: currentHost.socks5ProxyChain,
|
||||
});
|
||||
|
||||
if (result?.requires_warpgate) {
|
||||
setWarpgateRequired(true);
|
||||
setWarpgateSessionId(sessionId);
|
||||
setWarpgateUrl(result.url || "");
|
||||
setWarpgateSecurityKey(result.securityKey || "N/A");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result?.requires_totp) {
|
||||
setTotpRequired(true);
|
||||
setTotpSessionId(sessionId);
|
||||
@@ -380,8 +420,49 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
} catch (dirError: unknown) {
|
||||
console.error("Failed to load initial directory:", dirError);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
} catch (error: any) {
|
||||
console.error("SSH connection failed:", error);
|
||||
|
||||
if (error?.connectionLogs) {
|
||||
error.connectionLogs.forEach((log: any) => {
|
||||
addLog({
|
||||
type: log.type,
|
||||
stage: log.stage,
|
||||
message: log.message,
|
||||
details: log.details,
|
||||
});
|
||||
});
|
||||
if (error.requires_totp) {
|
||||
setTotpRequired(true);
|
||||
setTotpSessionId(error.sessionId || currentHost.id.toString());
|
||||
setTotpPrompt(
|
||||
error.prompt || t("fileManager.verificationCodePrompt"),
|
||||
);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
if (error.requires_warpgate) {
|
||||
setWarpgateRequired(true);
|
||||
setWarpgateSessionId(error.sessionId || currentHost.id.toString());
|
||||
setWarpgateUrl(error.url || "");
|
||||
setWarpgateSecurityKey(error.securityKey || "N/A");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
if (error.status === "auth_required") {
|
||||
setAuthDialogReason(error.reason || "no_keyboard");
|
||||
setShowAuthDialog(true);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
addLog({
|
||||
type: "error",
|
||||
stage: "connection",
|
||||
message: error?.message || t("fileManager.failedToConnect"),
|
||||
});
|
||||
}
|
||||
|
||||
handleCloseWithError(
|
||||
t("fileManager.failedToConnect") + ": " + (error.message || error),
|
||||
);
|
||||
@@ -392,14 +473,14 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
}
|
||||
|
||||
const loadDirectory = useCallback(
|
||||
async (path: string) => {
|
||||
async (path: string): Promise<boolean> => {
|
||||
if (!sshSessionId) {
|
||||
console.error("Cannot load directory: no SSH session ID");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isLoading && currentLoadingPathRef.current !== path) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
currentLoadingPathRef.current = path;
|
||||
@@ -411,7 +492,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
const response = await listSSHFiles(sshSessionId, path);
|
||||
|
||||
if (currentLoadingPathRef.current !== path) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const files = Array.isArray(response)
|
||||
@@ -420,29 +501,58 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
|
||||
setFiles(files);
|
||||
clearSelection();
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
if (currentLoadingPathRef.current === path) {
|
||||
const axiosError = error as {
|
||||
response?: {
|
||||
status?: number;
|
||||
data?: {
|
||||
needsSudo?: boolean;
|
||||
error?: string;
|
||||
sudoFailed?: boolean;
|
||||
};
|
||||
};
|
||||
message?: string;
|
||||
};
|
||||
|
||||
if (axiosError.response?.data?.needsSudo) {
|
||||
if (!sudoDialogOpen) {
|
||||
setPendingSudoOperation({ type: "navigate", path });
|
||||
setSudoDialogOpen(true);
|
||||
}
|
||||
|
||||
if (axiosError.response.data.sudoFailed) {
|
||||
toast.error(t("fileManager.sudoAuthFailed"));
|
||||
} else {
|
||||
toast.error(t("fileManager.permissionDenied"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
console.error("Failed to load directory:", error);
|
||||
|
||||
const errorMessage =
|
||||
axiosError.response?.data?.error ||
|
||||
axiosError.message ||
|
||||
String(error);
|
||||
|
||||
if (initialLoadDoneRef.current) {
|
||||
toast.error(
|
||||
t("fileManager.failedToLoadDirectory") +
|
||||
": " +
|
||||
(error.message || error),
|
||||
t("fileManager.failedToLoadDirectory") + ": " + errorMessage,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("SSH")
|
||||
errorMessage?.includes("connection") ||
|
||||
errorMessage?.includes("SSH")
|
||||
) {
|
||||
handleCloseWithError(
|
||||
t("fileManager.failedToLoadDirectory") +
|
||||
": " +
|
||||
(error.message || error),
|
||||
t("fileManager.failedToLoadDirectory") + ": " + errorMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
if (currentLoadingPathRef.current === path) {
|
||||
setIsLoading(false);
|
||||
@@ -450,7 +560,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
}
|
||||
}
|
||||
},
|
||||
[sshSessionId, isLoading, clearSelection, t],
|
||||
[sshSessionId, isLoading, clearSelection, t, sudoDialogOpen],
|
||||
);
|
||||
|
||||
const debouncedLoadDirectory = useCallback(
|
||||
@@ -720,9 +830,18 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
handleRefreshDirectory();
|
||||
clearSelection();
|
||||
} catch (error: unknown) {
|
||||
const axiosError = error as {
|
||||
response?: { data?: { needsSudo?: boolean; error?: string } };
|
||||
message?: string;
|
||||
};
|
||||
if (axiosError.response?.data?.needsSudo) {
|
||||
setPendingSudoOperation({ type: "delete", files });
|
||||
setSudoDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
axiosError.message?.includes("connection") ||
|
||||
axiosError.message?.includes("established")
|
||||
) {
|
||||
toast.error(
|
||||
`SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`,
|
||||
@@ -737,6 +856,57 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSudoPasswordSubmit(password: string) {
|
||||
if (!sshSessionId || !pendingSudoOperation) return;
|
||||
|
||||
try {
|
||||
await setSudoPassword(sshSessionId, password);
|
||||
setSudoDialogOpen(false);
|
||||
|
||||
if (pendingSudoOperation.type === "delete") {
|
||||
for (const file of pendingSudoOperation.files) {
|
||||
await deleteSSHItem(
|
||||
sshSessionId,
|
||||
file.path,
|
||||
file.type === "directory",
|
||||
currentHost?.id,
|
||||
currentHost?.userId?.toString(),
|
||||
);
|
||||
}
|
||||
toast.success(
|
||||
t("fileManager.itemsDeletedSuccessfully", {
|
||||
count: pendingSudoOperation.files.length,
|
||||
}),
|
||||
);
|
||||
handleRefreshDirectory();
|
||||
clearSelection();
|
||||
} else if (pendingSudoOperation.type === "navigate") {
|
||||
const success = await loadDirectory(pendingSudoOperation.path);
|
||||
if (success) {
|
||||
setCurrentPath(pendingSudoOperation.path);
|
||||
setPendingSudoOperation(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setPendingSudoOperation(null);
|
||||
} catch (error: unknown) {
|
||||
const axiosError = error as {
|
||||
response?: { data?: { needsSudo?: boolean; sudoFailed?: boolean } };
|
||||
message?: string;
|
||||
};
|
||||
|
||||
if (axiosError.response?.data?.sudoFailed) {
|
||||
toast.error(t("fileManager.sudoAuthFailed"));
|
||||
setSudoDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error(axiosError.message || t("fileManager.sudoOperationFailed"));
|
||||
setPendingSudoOperation(null);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreateNewFolder() {
|
||||
const defaultName = generateUniqueName(
|
||||
t("fileManager.newFolderDefault"),
|
||||
@@ -1472,6 +1642,57 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
if (onClose) onClose();
|
||||
}
|
||||
|
||||
async function handleWarpgateContinue() {
|
||||
if (!warpgateSessionId) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const result = await verifySSHWarpgate(warpgateSessionId);
|
||||
|
||||
if (result?.status === "success") {
|
||||
setWarpgateRequired(false);
|
||||
setWarpgateUrl("");
|
||||
setWarpgateSecurityKey("");
|
||||
setSshSessionId(warpgateSessionId);
|
||||
setWarpgateSessionId(null);
|
||||
|
||||
try {
|
||||
const response = await listSSHFiles(warpgateSessionId, currentPath);
|
||||
const files = Array.isArray(response)
|
||||
? response
|
||||
: response?.files || [];
|
||||
setFiles(files);
|
||||
clearSelection();
|
||||
initialLoadDoneRef.current = true;
|
||||
toast.success(t("fileManager.connectedSuccessfully"));
|
||||
|
||||
logFileManagerActivity();
|
||||
} catch (dirError: unknown) {
|
||||
console.error("Failed to load initial directory:", dirError);
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error("Warpgate verification failed:", error);
|
||||
toast.error(t("fileManager.warpgateVerificationFailed"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleWarpgateCancel() {
|
||||
setWarpgateRequired(false);
|
||||
setWarpgateUrl("");
|
||||
setWarpgateSecurityKey("");
|
||||
setWarpgateSessionId(null);
|
||||
if (onClose) onClose();
|
||||
}
|
||||
|
||||
function handleWarpgateOpenUrl() {
|
||||
if (warpgateUrl) {
|
||||
window.open(warpgateUrl, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAuthDialogSubmit(credentials: {
|
||||
password?: string;
|
||||
sshKey?: string;
|
||||
@@ -1505,6 +1726,15 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
socks5ProxyChain: currentHost.socks5ProxyChain,
|
||||
});
|
||||
|
||||
if (result?.requires_warpgate) {
|
||||
setWarpgateRequired(true);
|
||||
setWarpgateSessionId(sessionId);
|
||||
setWarpgateUrl(result.url || "");
|
||||
setWarpgateSecurityKey(result.securityKey || "N/A");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result?.requires_totp) {
|
||||
setTotpRequired(true);
|
||||
setTotpSessionId(sessionId);
|
||||
@@ -1928,9 +2158,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
}, [currentHost?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Saving viewMode to localStorage:", viewMode);
|
||||
localStorage.setItem("fileManagerViewMode", viewMode);
|
||||
console.log("Saved value:", localStorage.getItem("fileManagerViewMode"));
|
||||
}, [viewMode]);
|
||||
|
||||
const filteredFiles = files.filter((file) =>
|
||||
@@ -1950,52 +2178,169 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-canvas">
|
||||
<div className="flex-shrink-0 border-b border-edge">
|
||||
<div className="flex items-center justify-between p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="font-semibold text-foreground">
|
||||
{currentHost.name}
|
||||
</h2>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{currentHost.ip}:{currentHost.port}
|
||||
</span>
|
||||
<div className="h-full flex flex-col bg-canvas relative">
|
||||
<div
|
||||
className="h-full w-full flex flex-col"
|
||||
style={{
|
||||
visibility: isConnectionLogExpanded ? "hidden" : "visible",
|
||||
}}
|
||||
>
|
||||
<div className="flex-shrink-0 border-b border-edge">
|
||||
<div className="flex items-center justify-between p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="font-semibold text-foreground">
|
||||
{currentHost.name}
|
||||
</h2>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{currentHost.ip}:{currentHost.port}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t("fileManager.searchFiles")}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-8 w-48 h-9 bg-button border-edge"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex border border-edge rounded-md">
|
||||
<Button
|
||||
variant={viewMode === "grid" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewMode("grid")}
|
||||
className="rounded-r-none h-9"
|
||||
>
|
||||
<Grid3X3 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === "list" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewMode("list")}
|
||||
className="rounded-l-none h-9"
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = true;
|
||||
input.onchange = (e) => {
|
||||
const files = (e.target as HTMLInputElement).files;
|
||||
if (files) handleFilesDropped(files);
|
||||
};
|
||||
input.click();
|
||||
}}
|
||||
className="h-9"
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
{t("fileManager.upload")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCreateNewFolder}
|
||||
className="h-9"
|
||||
>
|
||||
<FolderPlus className="w-4 h-4 mr-2" />
|
||||
{t("fileManager.newFolder")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCreateNewFile}
|
||||
className="h-9"
|
||||
>
|
||||
<FilePlus className="w-4 h-4 mr-2" />
|
||||
{t("fileManager.newFile")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefreshDirectory}
|
||||
className="h-9"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex" {...dragHandlers}>
|
||||
<div className="w-64 flex-shrink-0 h-full">
|
||||
<FileManagerSidebar
|
||||
currentHost={currentHost}
|
||||
currentPath={currentPath}
|
||||
onPathChange={setCurrentPath}
|
||||
onLoadDirectory={loadDirectory}
|
||||
onFileOpen={handleSidebarFileOpen}
|
||||
sshSessionId={sshSessionId}
|
||||
refreshTrigger={sidebarRefreshTrigger}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t("fileManager.searchFiles")}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-8 w-48 h-9 bg-button border-edge"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 relative">
|
||||
<FileManagerGrid
|
||||
files={filteredFiles}
|
||||
selectedFiles={selectedFiles}
|
||||
onFileSelect={() => {}}
|
||||
onFileOpen={handleFileOpen}
|
||||
onSelectionChange={setSelection}
|
||||
currentPath={currentPath}
|
||||
isLoading={isLoading}
|
||||
onPathChange={setCurrentPath}
|
||||
onRefresh={handleRefreshDirectory}
|
||||
onUpload={handleFilesDropped}
|
||||
onDownload={(files) => files.forEach(handleDownloadFile)}
|
||||
onContextMenu={handleContextMenu}
|
||||
viewMode={viewMode}
|
||||
onRename={handleRenameConfirm}
|
||||
editingFile={editingFile}
|
||||
onStartEdit={handleStartEdit}
|
||||
onCancelEdit={handleCancelEdit}
|
||||
onDelete={handleDeleteFiles}
|
||||
onCopy={handleCopyFiles}
|
||||
onCut={handleCutFiles}
|
||||
onPaste={handlePasteFiles}
|
||||
onUndo={handleUndo}
|
||||
hasClipboard={!!clipboard}
|
||||
onFileDrop={handleFileDrop}
|
||||
onFileDiff={handleFileDiff}
|
||||
onSystemDragStart={handleFileDragStart}
|
||||
onSystemDragEnd={handleFileDragEnd}
|
||||
createIntent={createIntent}
|
||||
onConfirmCreate={handleConfirmCreate}
|
||||
onCancelCreate={handleCancelCreate}
|
||||
onNewFile={handleCreateNewFile}
|
||||
onNewFolder={handleCreateNewFolder}
|
||||
/>
|
||||
|
||||
<div className="flex border border-edge rounded-md">
|
||||
<Button
|
||||
variant={viewMode === "grid" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewMode("grid")}
|
||||
className="rounded-r-none h-9"
|
||||
>
|
||||
<Grid3X3 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === "list" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
onClick={() => setViewMode("list")}
|
||||
className="rounded-l-none h-9"
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
<FileManagerContextMenu
|
||||
x={contextMenu.x}
|
||||
y={contextMenu.y}
|
||||
files={contextMenu.files}
|
||||
isVisible={contextMenu.isVisible}
|
||||
onClose={() =>
|
||||
setContextMenu((prev) => ({ ...prev, isVisible: false }))
|
||||
}
|
||||
onDownload={(files) => files.forEach(handleDownloadFile)}
|
||||
onRename={handleRenameFile}
|
||||
onCopy={handleCopyFiles}
|
||||
onCut={handleCutFiles}
|
||||
onPaste={handlePasteFiles}
|
||||
onDelete={handleDeleteFiles}
|
||||
onUpload={() => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = true;
|
||||
@@ -2005,137 +2350,27 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
};
|
||||
input.click();
|
||||
}}
|
||||
className="h-9"
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
{t("fileManager.upload")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCreateNewFolder}
|
||||
className="h-9"
|
||||
>
|
||||
<FolderPlus className="w-4 h-4 mr-2" />
|
||||
{t("fileManager.newFolder")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCreateNewFile}
|
||||
className="h-9"
|
||||
>
|
||||
<FilePlus className="w-4 h-4 mr-2" />
|
||||
{t("fileManager.newFile")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefreshDirectory}
|
||||
className="h-9"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</Button>
|
||||
onNewFolder={handleCreateNewFolder}
|
||||
onNewFile={handleCreateNewFile}
|
||||
onRefresh={handleRefreshDirectory}
|
||||
hasClipboard={!!clipboard}
|
||||
onDragToDesktop={() => handleDragToDesktop(contextMenu.files)}
|
||||
onOpenTerminal={(path) => handleOpenTerminal(path)}
|
||||
onRunExecutable={(file) => handleRunExecutable(file)}
|
||||
onPinFile={handlePinFile}
|
||||
onUnpinFile={handleUnpinFile}
|
||||
onAddShortcut={handleAddShortcut}
|
||||
isPinned={isPinnedFile}
|
||||
currentPath={currentPath}
|
||||
onProperties={handleOpenPermissionsDialog}
|
||||
onExtractArchive={handleExtractArchive}
|
||||
onCompress={handleOpenCompressDialog}
|
||||
onCopyPath={handleCopyPath}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex" {...dragHandlers}>
|
||||
<div className="w-64 flex-shrink-0 h-full">
|
||||
<FileManagerSidebar
|
||||
currentHost={currentHost}
|
||||
currentPath={currentPath}
|
||||
onPathChange={setCurrentPath}
|
||||
onLoadDirectory={loadDirectory}
|
||||
onFileOpen={handleSidebarFileOpen}
|
||||
sshSessionId={sshSessionId}
|
||||
refreshTrigger={sidebarRefreshTrigger}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 relative">
|
||||
<FileManagerGrid
|
||||
files={filteredFiles}
|
||||
selectedFiles={selectedFiles}
|
||||
onFileSelect={() => {}}
|
||||
onFileOpen={handleFileOpen}
|
||||
onSelectionChange={setSelection}
|
||||
currentPath={currentPath}
|
||||
isLoading={isLoading}
|
||||
onPathChange={setCurrentPath}
|
||||
onRefresh={handleRefreshDirectory}
|
||||
onUpload={handleFilesDropped}
|
||||
onDownload={(files) => files.forEach(handleDownloadFile)}
|
||||
onContextMenu={handleContextMenu}
|
||||
viewMode={viewMode}
|
||||
onRename={handleRenameConfirm}
|
||||
editingFile={editingFile}
|
||||
onStartEdit={handleStartEdit}
|
||||
onCancelEdit={handleCancelEdit}
|
||||
onDelete={handleDeleteFiles}
|
||||
onCopy={handleCopyFiles}
|
||||
onCut={handleCutFiles}
|
||||
onPaste={handlePasteFiles}
|
||||
onUndo={handleUndo}
|
||||
hasClipboard={!!clipboard}
|
||||
onFileDrop={handleFileDrop}
|
||||
onFileDiff={handleFileDiff}
|
||||
onSystemDragStart={handleFileDragStart}
|
||||
onSystemDragEnd={handleFileDragEnd}
|
||||
createIntent={createIntent}
|
||||
onConfirmCreate={handleConfirmCreate}
|
||||
onCancelCreate={handleCancelCreate}
|
||||
onNewFile={handleCreateNewFile}
|
||||
onNewFolder={handleCreateNewFolder}
|
||||
/>
|
||||
|
||||
<FileManagerContextMenu
|
||||
x={contextMenu.x}
|
||||
y={contextMenu.y}
|
||||
files={contextMenu.files}
|
||||
isVisible={contextMenu.isVisible}
|
||||
onClose={() =>
|
||||
setContextMenu((prev) => ({ ...prev, isVisible: false }))
|
||||
}
|
||||
onDownload={(files) => files.forEach(handleDownloadFile)}
|
||||
onRename={handleRenameFile}
|
||||
onCopy={handleCopyFiles}
|
||||
onCut={handleCutFiles}
|
||||
onPaste={handlePasteFiles}
|
||||
onDelete={handleDeleteFiles}
|
||||
onUpload={() => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = true;
|
||||
input.onchange = (e) => {
|
||||
const files = (e.target as HTMLInputElement).files;
|
||||
if (files) handleFilesDropped(files);
|
||||
};
|
||||
input.click();
|
||||
}}
|
||||
onNewFolder={handleCreateNewFolder}
|
||||
onNewFile={handleCreateNewFile}
|
||||
onRefresh={handleRefreshDirectory}
|
||||
hasClipboard={!!clipboard}
|
||||
onDragToDesktop={() => handleDragToDesktop(contextMenu.files)}
|
||||
onOpenTerminal={(path) => handleOpenTerminal(path)}
|
||||
onRunExecutable={(file) => handleRunExecutable(file)}
|
||||
onPinFile={handlePinFile}
|
||||
onUnpinFile={handleUnpinFile}
|
||||
onAddShortcut={handleAddShortcut}
|
||||
isPinned={isPinnedFile}
|
||||
currentPath={currentPath}
|
||||
onProperties={handleOpenPermissionsDialog}
|
||||
onExtractArchive={handleExtractArchive}
|
||||
onCompress={handleOpenCompressDialog}
|
||||
onCopyPath={handleCopyPath}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CompressDialog
|
||||
open={compressDialogFiles.length > 0}
|
||||
onOpenChange={(open) => !open && setCompressDialogFiles([])}
|
||||
@@ -2150,6 +2385,15 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
onCancel={handleTotpCancel}
|
||||
/>
|
||||
|
||||
<WarpgateDialog
|
||||
isOpen={warpgateRequired}
|
||||
url={warpgateUrl}
|
||||
securityKey={warpgateSecurityKey}
|
||||
onContinue={handleWarpgateContinue}
|
||||
onCancel={handleWarpgateCancel}
|
||||
onOpenUrl={handleWarpgateOpenUrl}
|
||||
/>
|
||||
|
||||
{currentHost && (
|
||||
<SSHAuthDialog
|
||||
isOpen={showAuthDialog}
|
||||
@@ -2173,14 +2417,41 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
}}
|
||||
onSave={handleSavePermissions}
|
||||
/>
|
||||
|
||||
<SudoPasswordDialog
|
||||
open={sudoDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setSudoDialogOpen(open);
|
||||
if (!open) setPendingSudoOperation(null);
|
||||
}}
|
||||
onSubmit={handleSudoPasswordSubmit}
|
||||
/>
|
||||
<SimpleLoader
|
||||
visible={(isReconnecting || isLoading) && !isConnectionLogExpanded}
|
||||
message={t("fileManager.connecting")}
|
||||
/>
|
||||
<ConnectionLog
|
||||
isConnecting={isReconnecting || isLoading}
|
||||
isConnected={!!sshSessionId}
|
||||
hasConnectionError={hasConnectionError}
|
||||
position={hasConnectionError ? "top" : "bottom"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FileManager({ initialHost, onClose }: FileManagerProps) {
|
||||
function FileManagerInner({ initialHost, onClose }: FileManagerProps) {
|
||||
return (
|
||||
<WindowManager>
|
||||
<FileManagerContent initialHost={initialHost} onClose={onClose} />
|
||||
</WindowManager>
|
||||
);
|
||||
}
|
||||
|
||||
export function FileManager(props: FileManagerProps) {
|
||||
return (
|
||||
<ConnectionLogProvider>
|
||||
<FileManagerInner {...props} />
|
||||
</ConnectionLogProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user