From bc713aca7d5d342323d7c2ed36ac1ba38d969633 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Wed, 20 May 2026 14:55:41 -0500 Subject: [PATCH] feat: terminal theme improvemnts, file manager improvements, host manager improvements (ready for initial user testing?) --- src/backend/database/routes/host.ts | 4 +- src/types/ui-types.ts | 2 + src/ui/AppShell.tsx | 101 +++---- src/ui/components/slider.tsx | 8 +- src/ui/features/file-manager/FileManager.tsx | 19 +- .../features/file-manager/FileManagerGrid.tsx | 10 - src/ui/features/guacamole/GuacamoleApp.tsx | 2 +- src/ui/features/terminal/Terminal.tsx | 93 +++--- src/ui/locales/en.json | 14 + src/ui/main-axios.ts | 179 +---------- src/ui/shell/tabUtils.tsx | 59 +++- src/ui/sidebar/HostManager.tsx | 283 ++++++++++++------ src/ui/sidebar/SidebarTree.tsx | 77 ++--- src/ui/ssh/dialogs/WarpgateDialog.tsx | 6 +- 14 files changed, 406 insertions(+), 451 deletions(-) diff --git a/src/backend/database/routes/host.ts b/src/backend/database/routes/host.ts index b4467265..d998f752 100644 --- a/src/backend/database/routes/host.ts +++ b/src/backend/database/routes/host.ts @@ -739,7 +739,7 @@ router.post( portKnockSequence: portKnockSequence ? JSON.stringify(portKnockSequence) : null, - enableSsh: enableSsh !== false ? 1 : 0, + enableSsh: enableSsh ? 1 : 0, enableRdp: enableRdp ? 1 : 0, enableVnc: enableVnc ? 1 : 0, enableTelnet: enableTelnet ? 1 : 0, @@ -1274,7 +1274,7 @@ router.put( portKnockSequence: portKnockSequence ? JSON.stringify(portKnockSequence) : null, - enableSsh: enableSsh !== false ? 1 : 0, + enableSsh: enableSsh ? 1 : 0, enableRdp: enableRdp ? 1 : 0, enableVnc: enableVnc ? 1 : 0, enableTelnet: enableTelnet ? 1 : 0, diff --git a/src/types/ui-types.ts b/src/types/ui-types.ts index 62c2bbd6..7a898f2c 100644 --- a/src/types/ui-types.ts +++ b/src/types/ui-types.ts @@ -14,6 +14,8 @@ export type Host = { credentialId?: string; overrideCredentialUsername?: boolean; password?: string; + hasPassword?: boolean; + hasKey?: boolean; key?: string; keyPassword?: string; keyType?: string; diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx index 9bc83199..f387c656 100644 --- a/src/ui/AppShell.tsx +++ b/src/ui/AppShell.tsx @@ -265,6 +265,11 @@ export function AppShell({ loadHosts(); }, [loadHosts]); + useEffect(() => { + window.addEventListener("termix:hosts-changed", loadHosts); + return () => window.removeEventListener("termix:hosts-changed", loadHosts); + }, [loadHosts]); + // Sync tab host data when allHosts updates (e.g. after editing terminal theme in host settings) useEffect(() => { if (allHosts.length === 0) return; @@ -293,37 +298,29 @@ export function AppShell({ // ─── Tab management ────────────────────────────────────────────────────── function openTab(host: Host, type: TabType) { - const same = tabs.filter( - (t) => t.type === type && t.label.replace(/ \(\d+\)$/, "") === host.name, - ); - if (same.length === 0) { - const tabId = `${host.name}-${type}`; - const ref = type === "terminal" ? createRef() : undefined; - if (ref) terminalRefs.current.set(tabId, ref); - const tab = { id: tabId, type, label: host.name, host, terminalRef: ref }; - setTabs((prev) => [...prev, tab]); - setActiveTabId(tab.id); - return; - } const tabId = `${host.name}-${type}-${Date.now()}`; const ref = type === "terminal" ? createRef() : undefined; if (ref) terminalRefs.current.set(tabId, ref); - const tab = { - id: tabId, - type, - label: `${host.name} (${same.length + 1})`, - host, - terminalRef: ref, - }; + setTabs((prev) => { - const next = prev.map((t) => - t.id === same[0].id && !/\(\d+\)$/.test(t.label) - ? { ...t, label: `${host.name} (1)`, host } - : t, + const same = prev.filter( + (t) => + t.type === type && t.label.replace(/ \(\d+\)$/, "") === host.name, ); - return [...next, tab]; + const label = + same.length === 0 ? host.name : `${host.name} (${same.length + 1})`; + + // Retrofit the first duplicate's label to "(1)" if needed + const next = + same.length === 1 && !/\(\d+\)$/.test(same[0].label) + ? prev.map((t) => + t.id === same[0].id ? { ...t, label: `${host.name} (1)` } : t, + ) + : prev; + + return [...next, { id: tabId, type, label, host, terminalRef: ref }]; }); - setActiveTabId(tab.id); + setActiveTabId(tabId); } function connectHost(host: Host, preferredType?: TabType) { @@ -674,36 +671,30 @@ export function AppShell({ ) : ( <> {/* Terminal tabs: always in DOM, absolutely positioned so xterm always has real dimensions */} - {(() => { - const activeTab = tabs.find((t) => t.id === activeTabId); - const nonTerminalActive = - activeTab && activeTab.type !== "terminal"; - return tabs - .filter((tab) => tab.type === "terminal") - .map((tab) => { - const visible = tab.id === activeTabId; - return ( -
- {renderTabContent( - tab, - openSingletonTab, - openTab, - closeTab, - visible, - )} -
- ); - }); - })()} + {tabs + .filter((tab) => tab.type === "terminal") + .map((tab) => { + const visible = tab.id === activeTabId; + return ( +
+ {renderTabContent( + tab, + openSingletonTab, + openTab, + closeTab, + visible, + )} +
+ ); + })} {/* Non-terminal tabs: absolutely positioned above terminals when active */} {tabs .filter((tab) => tab.type !== "terminal") diff --git a/src/ui/components/slider.tsx b/src/ui/components/slider.tsx index 231c5a81..794a3f93 100644 --- a/src/ui/components/slider.tsx +++ b/src/ui/components/slider.tsx @@ -37,21 +37,23 @@ function Slider({ {Array.from({ length: _values.length }, (_, index) => ( ))} diff --git a/src/ui/features/file-manager/FileManager.tsx b/src/ui/features/file-manager/FileManager.tsx index 8f799b2f..11c35228 100644 --- a/src/ui/features/file-manager/FileManager.tsx +++ b/src/ui/features/file-manager/FileManager.tsx @@ -557,10 +557,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { return false; } - if (isLoading && currentLoadingPathRef.current !== path) { - return false; - } - let resolvedPath = path; if (path.includes("$") || path.startsWith("~")) { resolvedPath = await resolveSSHPath(sshSessionId, path); @@ -711,6 +707,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { const navigateTo = useCallback( (path: string) => { + if (sshSessionId) setIsLoading(true); setCurrentPath(path); setNavHistory((prev) => { const next = [...prev.slice(0, navIndex + 1), path]; @@ -718,24 +715,26 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { return next; }); }, - [navIndex], + [navIndex, sshSessionId], ); const goBack = useCallback(() => { if (navIndex > 0) { + if (sshSessionId) setIsLoading(true); const newIndex = navIndex - 1; setNavIndex(newIndex); setCurrentPath(navHistory[newIndex]); } - }, [navIndex, navHistory]); + }, [navIndex, navHistory, sshSessionId]); const goForward = useCallback(() => { if (navIndex < navHistory.length - 1) { + if (sshSessionId) setIsLoading(true); const newIndex = navIndex + 1; setNavIndex(newIndex); setCurrentPath(navHistory[newIndex]); } - }, [navIndex, navHistory]); + }, [navIndex, navHistory, sshSessionId]); const goUp = useCallback(() => { if (currentPath === "/") return; @@ -1291,6 +1290,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { async function handleFileOpen(file: FileItem) { if (file.type === "directory") { + if (sshSessionId) setIsLoading(true); setCurrentPath(file.path); } else if (file.type === "link") { await handleSymlinkClick(file); @@ -2599,11 +2599,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { variant="ghost" size="icon" onClick={handleRefreshDirectory} - disabled={isLoading} className="size-8 rounded-none" > @@ -2878,8 +2877,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { onFileOpen={handleFileOpen} onSelectionChange={setSelection} currentPath={currentPath} - isLoading={isLoading} - isConnected={!!sshSessionId} onPathChange={navigateTo} onRefresh={handleRefreshDirectory} onUpload={handleFilesDropped} diff --git a/src/ui/features/file-manager/FileManagerGrid.tsx b/src/ui/features/file-manager/FileManagerGrid.tsx index cddde88f..a25698f4 100644 --- a/src/ui/features/file-manager/FileManagerGrid.tsx +++ b/src/ui/features/file-manager/FileManagerGrid.tsx @@ -21,7 +21,6 @@ import { } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { FileItem } from "@/types/index"; -import { SimpleLoader } from "@/lib/SimpleLoader.tsx"; interface CreateIntent { id: string; @@ -66,8 +65,6 @@ interface FileManagerGridProps { onFileOpen: (file: FileItem) => void; onSelectionChange: (files: FileItem[]) => void; currentPath: string; - isLoading?: boolean; - isConnected?: boolean; onPathChange: (path: string) => void; onRefresh: () => void; onUpload?: (files: FileList) => void; @@ -191,8 +188,6 @@ export function FileManagerGrid({ onFileOpen, onSelectionChange, currentPath, - isLoading, - isConnected, onPathChange, onRefresh, onUpload, @@ -1240,11 +1235,6 @@ export function FileManagerGrid({ , document.body, )} - - ); } diff --git a/src/ui/features/guacamole/GuacamoleApp.tsx b/src/ui/features/guacamole/GuacamoleApp.tsx index 00ce371e..17ef8022 100644 --- a/src/ui/features/guacamole/GuacamoleApp.tsx +++ b/src/ui/features/guacamole/GuacamoleApp.tsx @@ -83,7 +83,7 @@ const GuacamoleAppInner: React.FC = ({ .then((status) => { if (status.guacd.status !== "connected") { setError( - "Remote desktop service (guacd) is not available. Please ensure guacd is running and accessible.", + "Remote desktop service (guacd) is not available. Please ensure guacd is running and accessible and configured properly in admin settings.", ); return; } diff --git a/src/ui/features/terminal/Terminal.tsx b/src/ui/features/terminal/Terminal.tsx index 46d5d736..45209b1b 100644 --- a/src/ui/features/terminal/Terminal.tsx +++ b/src/ui/features/terminal/Terminal.tsx @@ -53,6 +53,46 @@ import { ConnectionLog } from "@/ssh/connection-log/ConnectionLog.tsx"; import { toast } from "sonner"; import { Button } from "@/components/button"; +// Background/foreground per UI theme for "Termix Default" — must match index.css +const TERMIX_DEFAULT_COLORS: Record< + string, + { background: string; foreground: string } +> = { + dark: { background: "#0c0d0b", foreground: "#fafafa" }, + light: { background: "#ffffff", foreground: "#111210" }, + dracula: { background: "#282a36", foreground: "#f8f8f2" }, + catppuccin: { background: "#1e1e2e", foreground: "#cdd6f4" }, + nord: { background: "#2e3440", foreground: "#eceff4" }, + solarized: { background: "#002b36", foreground: "#839496" }, + "tokyo-night": { background: "#1a1b26", foreground: "#a9b1d6" }, + "one-dark": { background: "#282c34", foreground: "#abb2bf" }, + gruvbox: { background: "#282828", foreground: "#ebdbb2" }, +}; + +function resolveTermixThemeColors(activeTheme: string, appTheme: string) { + if (activeTheme !== "termix") { + return ( + TERMINAL_THEMES[activeTheme]?.colors || TERMINAL_THEMES.termixDark.colors + ); + } + let resolvedUiTheme = appTheme; + if (appTheme === "system") { + resolvedUiTheme = window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; + } + const uiColors = + TERMIX_DEFAULT_COLORS[resolvedUiTheme] ?? TERMIX_DEFAULT_COLORS.dark; + const base = TERMINAL_THEMES.termixDark.colors; + return { + ...base, + background: uiColors.background, + foreground: uiColors.foreground, + cursor: uiColors.foreground, + cursorAccent: uiColors.background, + }; +} + interface HostConfig { id?: number; instanceId?: string; @@ -130,27 +170,8 @@ const TerminalInner = forwardRef( DEFAULT_TERMINAL_CONFIG.theme, }; - const isDarkMode = - appTheme === "dark" || - appTheme === "dracula" || - appTheme === "gentlemansChoice" || - appTheme === "midnightEspresso" || - appTheme === "catppuccinMocha" || - (appTheme === "system" && - window.matchMedia("(prefers-color-scheme: dark)").matches); - - let themeColors; const activeTheme = previewTheme || config.theme; - - if (activeTheme === "termix") { - themeColors = isDarkMode - ? TERMINAL_THEMES.termixDark.colors - : TERMINAL_THEMES.termixLight.colors; - } else { - themeColors = - TERMINAL_THEMES[activeTheme]?.colors || - TERMINAL_THEMES.termixDark.colors; - } + const themeColors = resolveTermixThemeColors(activeTheme, appTheme); const backgroundColor = themeColors.background; const fitAddonRef = useRef(null); const webSocketRef = useRef(null); @@ -1828,18 +1849,8 @@ const TerminalInner = forwardRef( ...hostConfig.terminalConfig, }; - let themeColors; const activeTheme = previewTheme || config.theme; - - if (activeTheme === "termix") { - themeColors = isDarkMode - ? TERMINAL_THEMES.termixDark.colors - : TERMINAL_THEMES.termixLight.colors; - } else { - themeColors = - TERMINAL_THEMES[activeTheme]?.colors || - TERMINAL_THEMES.termixDark.colors; - } + const themeColors = resolveTermixThemeColors(activeTheme, appTheme); const fontConfig = TERMINAL_FONTS.find( (f) => f.value === config.fontFamily, @@ -1896,13 +1907,7 @@ const TerminalInner = forwardRef( // Refresh terminal to apply new theme colors to existing buffer content hardRefresh(); - }, [ - terminal, - hostConfig.terminalConfig, - previewTheme, - isDarkMode, - isFitted, - ]); + }, [terminal, hostConfig.terminalConfig, previewTheme, appTheme, isFitted]); useEffect(() => { if (!terminal || !xtermRef.current) return; @@ -1917,18 +1922,8 @@ const TerminalInner = forwardRef( ); const fontFamily = fontConfig?.fallback || TERMINAL_FONTS[0].fallback; - let themeColors; const activeTheme = previewTheme || config.theme; - - if (activeTheme === "termix") { - themeColors = isDarkMode - ? TERMINAL_THEMES.termixDark.colors - : TERMINAL_THEMES.termixLight.colors; - } else { - themeColors = - TERMINAL_THEMES[activeTheme]?.colors || - TERMINAL_THEMES.termixDark.colors; - } + const themeColors = resolveTermixThemeColors(activeTheme, appTheme); // Set initial options before opening the terminal terminal.options = { diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index 5303693a..e35e901b 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -171,6 +171,7 @@ "upload": "Upload", "authentication": "Authentication", "password": "Password", + "passwordSaved": "Password saved, type to change", "key": "Key", "credential": "Credential", "none": "None", @@ -435,11 +436,24 @@ "copiedToClipboard": "Copied to clipboard", "terminalUrlCopied": "Terminal URL copied", "fileManagerUrlCopied": "File Manager URL copied", + "tunnelUrlCopied": "Tunnel URL copied", + "dockerUrlCopied": "Docker URL copied", + "serverStatsUrlCopied": "Server Stats URL copied", + "rdpUrlCopied": "RDP URL copied", + "vncUrlCopied": "VNC URL copied", + "telnetUrlCopied": "Telnet URL copied", "remoteDesktopUrlCopied": "Remote Desktop URL copied", "cloneHostAction": "Clone Host", "copyAddress": "Copy Address", + "copyLink": "Copy Link", "copyTerminalUrlAction": "Copy Terminal URL", "copyFileManagerUrlAction": "Copy File Manager URL", + "copyTunnelUrlAction": "Copy Tunnel URL", + "copyDockerUrlAction": "Copy Docker URL", + "copyServerStatsUrlAction": "Copy Server Stats URL", + "copyRdpUrlAction": "Copy RDP URL", + "copyVncUrlAction": "Copy VNC URL", + "copyTelnetUrlAction": "Copy Telnet URL", "copyRemoteDesktopUrlAction": "Copy Remote Desktop URL", "deleteCredentialConfirm": "Delete credential \"{{name}}\"?", "deletedCredential": "Deleted {{name}}", diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index c0b47ba3..4f46aecb 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -1107,102 +1107,18 @@ export async function getSSHHosts(): Promise { export async function createSSHHost(hostData: SSHHostData): Promise { try { - const submitData = { - connectionType: hostData.connectionType || "ssh", - name: hostData.name || "", - ip: hostData.ip, - port: parseInt(hostData.port.toString()) || 22, - username: hostData.username, - folder: hostData.folder || "", - tags: hostData.tags || [], - pin: Boolean(hostData.pin), - authType: hostData.authType, - password: - hostData.connectionType !== "ssh" - ? hostData.password || null - : hostData.authType === "password" - ? hostData.password - : null, - key: hostData.authType === "key" ? hostData.key : null, - keyPassword: hostData.authType === "key" ? hostData.keyPassword : null, - keyType: hostData.authType === "key" ? hostData.keyType : null, - credentialId: - hostData.authType === "credential" ? hostData.credentialId : null, - overrideCredentialUsername: Boolean(hostData.overrideCredentialUsername), - enableTerminal: Boolean(hostData.enableTerminal), - enableTunnel: Boolean(hostData.enableTunnel), - enableFileManager: Boolean(hostData.enableFileManager), - enableDocker: Boolean(hostData.enableDocker), - showTerminalInSidebar: Boolean(hostData.showTerminalInSidebar), - showFileManagerInSidebar: Boolean(hostData.showFileManagerInSidebar), - showTunnelInSidebar: Boolean(hostData.showTunnelInSidebar), - showDockerInSidebar: Boolean(hostData.showDockerInSidebar), - showServerStatsInSidebar: Boolean(hostData.showServerStatsInSidebar), - defaultPath: hostData.defaultPath || "/", - tunnelConnections: hostData.tunnelConnections || [], - jumpHosts: hostData.jumpHosts || [], - quickActions: hostData.quickActions || [], - sudoPassword: hostData.sudoPassword || null, - statsConfig: hostData.statsConfig || null, - dockerConfig: hostData.dockerConfig || null, - terminalConfig: hostData.terminalConfig || null, - forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive), - domain: hostData.domain || null, - security: hostData.security || null, - ignoreCert: Boolean(hostData.ignoreCert), - guacamoleConfig: hostData.guacamoleConfig || null, - notes: hostData.notes || "", - useSocks5: Boolean(hostData.useSocks5), - socks5Host: hostData.socks5Host || null, - socks5Port: hostData.socks5Port || null, - socks5Username: hostData.socks5Username || null, - socks5Password: hostData.socks5Password || null, - socks5ProxyChain: hostData.socks5ProxyChain || null, - macAddress: hostData.macAddress || null, - portKnockSequence: hostData.portKnockSequence || null, - enableSsh: hostData.enableSsh !== false, - enableRdp: Boolean(hostData.enableRdp), - enableVnc: Boolean(hostData.enableVnc), - enableTelnet: Boolean(hostData.enableTelnet), - sshPort: hostData.sshPort || hostData.port || 22, - rdpPort: hostData.rdpPort || 3389, - vncPort: hostData.vncPort || 5900, - telnetPort: hostData.telnetPort || 23, - rdpUser: hostData.rdpUser || null, - rdpPassword: hostData.rdpPassword || null, - rdpDomain: hostData.rdpDomain || null, - rdpSecurity: hostData.rdpSecurity || null, - rdpIgnoreCert: Boolean(hostData.rdpIgnoreCert), - vncPassword: hostData.vncPassword || null, - vncUser: hostData.vncUser || null, - telnetUser: hostData.telnetUser || null, - telnetPassword: hostData.telnetPassword || null, - }; - - if (!submitData.enableTunnel) { - submitData.tunnelConnections = []; - } - - if (!submitData.enableFileManager) { - submitData.defaultPath = ""; - } - if (hostData.authType === "key" && hostData.key instanceof File) { const formData = new FormData(); formData.append("key", hostData.key); - - const dataWithoutFile = { ...submitData }; - delete dataWithoutFile.key; + const dataWithoutFile = { ...hostData, key: undefined }; formData.append("data", JSON.stringify(dataWithoutFile)); - const response = await sshHostApi.post("/db/host", formData, { headers: { "Content-Type": "multipart/form-data" }, }); return response.data; - } else { - const response = await sshHostApi.post("/db/host", submitData); - return response.data; } + const response = await sshHostApi.post("/db/host", hostData); + return response.data; } catch (error) { throw handleApiError(error, "create SSH host"); } @@ -1213,101 +1129,18 @@ export async function updateSSHHost( hostData: SSHHostData, ): Promise { try { - const submitData = { - connectionType: hostData.connectionType || "ssh", - name: hostData.name || "", - ip: hostData.ip, - port: parseInt(hostData.port.toString()) || 22, - username: hostData.username, - folder: hostData.folder || "", - tags: hostData.tags || [], - pin: Boolean(hostData.pin), - authType: hostData.authType, - password: - hostData.connectionType !== "ssh" - ? hostData.password || null - : hostData.authType === "password" - ? hostData.password - : null, - key: hostData.authType === "key" ? hostData.key : null, - keyPassword: hostData.authType === "key" ? hostData.keyPassword : null, - keyType: hostData.authType === "key" ? hostData.keyType : null, - credentialId: - hostData.authType === "credential" ? hostData.credentialId : null, - overrideCredentialUsername: Boolean(hostData.overrideCredentialUsername), - enableTerminal: Boolean(hostData.enableTerminal), - enableTunnel: Boolean(hostData.enableTunnel), - enableFileManager: Boolean(hostData.enableFileManager), - enableDocker: Boolean(hostData.enableDocker), - showTerminalInSidebar: Boolean(hostData.showTerminalInSidebar), - showFileManagerInSidebar: Boolean(hostData.showFileManagerInSidebar), - showTunnelInSidebar: Boolean(hostData.showTunnelInSidebar), - showDockerInSidebar: Boolean(hostData.showDockerInSidebar), - showServerStatsInSidebar: Boolean(hostData.showServerStatsInSidebar), - defaultPath: hostData.defaultPath || "/", - tunnelConnections: hostData.tunnelConnections || [], - jumpHosts: hostData.jumpHosts || [], - quickActions: hostData.quickActions || [], - sudoPassword: hostData.sudoPassword || null, - statsConfig: hostData.statsConfig || null, - dockerConfig: hostData.dockerConfig || null, - terminalConfig: hostData.terminalConfig || null, - forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive), - domain: hostData.domain || null, - security: hostData.security || null, - ignoreCert: Boolean(hostData.ignoreCert), - guacamoleConfig: hostData.guacamoleConfig || null, - notes: hostData.notes || "", - useSocks5: Boolean(hostData.useSocks5), - socks5Host: hostData.socks5Host || null, - socks5Port: hostData.socks5Port || null, - socks5Username: hostData.socks5Username || null, - socks5Password: hostData.socks5Password || null, - socks5ProxyChain: hostData.socks5ProxyChain || null, - macAddress: hostData.macAddress || null, - portKnockSequence: hostData.portKnockSequence || null, - enableSsh: hostData.enableSsh !== false, - enableRdp: Boolean(hostData.enableRdp), - enableVnc: Boolean(hostData.enableVnc), - enableTelnet: Boolean(hostData.enableTelnet), - sshPort: hostData.sshPort || hostData.port || 22, - rdpPort: hostData.rdpPort || 3389, - vncPort: hostData.vncPort || 5900, - telnetPort: hostData.telnetPort || 23, - rdpUser: hostData.rdpUser || null, - rdpPassword: hostData.rdpPassword || null, - rdpDomain: hostData.rdpDomain || null, - rdpSecurity: hostData.rdpSecurity || null, - rdpIgnoreCert: Boolean(hostData.rdpIgnoreCert), - vncPassword: hostData.vncPassword || null, - vncUser: hostData.vncUser || null, - telnetUser: hostData.telnetUser || null, - telnetPassword: hostData.telnetPassword || null, - }; - - if (!submitData.enableTunnel) { - submitData.tunnelConnections = []; - } - if (!submitData.enableFileManager) { - submitData.defaultPath = ""; - } - if (hostData.authType === "key" && hostData.key instanceof File) { const formData = new FormData(); formData.append("key", hostData.key); - - const dataWithoutFile = { ...submitData }; - delete dataWithoutFile.key; + const dataWithoutFile = { ...hostData, key: undefined }; formData.append("data", JSON.stringify(dataWithoutFile)); - const response = await sshHostApi.put(`/db/host/${hostId}`, formData, { headers: { "Content-Type": "multipart/form-data" }, }); return response.data; - } else { - const response = await sshHostApi.put(`/db/host/${hostId}`, submitData); - return response.data; } + const response = await sshHostApi.put(`/db/host/${hostId}`, hostData); + return response.data; } catch (error) { throw handleApiError(error, "update SSH host"); } diff --git a/src/ui/shell/tabUtils.tsx b/src/ui/shell/tabUtils.tsx index c31bc58c..d96f5f7e 100644 --- a/src/ui/shell/tabUtils.tsx +++ b/src/ui/shell/tabUtils.tsx @@ -23,6 +23,7 @@ import { TunnelTab } from "@/features/tunnel/TunnelTab"; import { NetworkGraphCard } from "@/dashboard/cards/NetworkGraphCard"; import type { Tab, TabType, Host } from "@/types/ui-types"; import type { SSHHost } from "@/types"; +import { useTabsSafe } from "@/shell/TabContext"; function hostToSSHHost(h: Host): SSHHost { return { @@ -109,6 +110,41 @@ export function tabIcon(type: TabType) { } } +function TerminalTabContent({ + tab, + host, + label, + isVisible, + onCloseTab, +}: { + tab: Tab; + host: Host; + label: string; + isVisible: boolean; + onCloseTab?: (id: string) => void; +}) { + const { previewTerminalTheme } = useTabsSafe(); + return ( + + onCloseTab?.(tab.id)} + previewTheme={previewTerminalTheme} + /> + + ); +} + export function renderTabContent( tab: Tab, onOpenSingletonTab?: (type: TabType) => void, @@ -136,22 +172,13 @@ export function renderTabContent( /> ); return ( - - onCloseTab?.(tab.id)} - /> - + ); case "files": diff --git a/src/ui/sidebar/HostManager.tsx b/src/ui/sidebar/HostManager.tsx index 7ab38a69..de051d01 100644 --- a/src/ui/sidebar/HostManager.tsx +++ b/src/ui/sidebar/HostManager.tsx @@ -14,6 +14,7 @@ import { } from "@/lib/terminal-themes"; import { Button } from "@/components/button"; import { Input } from "@/components/input"; +import { PasswordInput } from "@/components/password-input"; import { Slider } from "@/components/slider"; import { Activity, @@ -32,6 +33,7 @@ import { Info, KeyRound, LayoutDashboard, + Link, ListChecks, Lock, MoreHorizontal, @@ -60,6 +62,9 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/dropdown-menu"; import { toast } from "sonner"; @@ -98,6 +103,7 @@ import { import type { SSHHostWithStatus } from "@/main-axios"; import type { Host, Credential } from "@/types/ui-types"; +import { useTabsSafe } from "@/shell/TabContext"; function sshHostToHost(h: SSHHostWithStatus): Host { const parseJson = (v: any) => { @@ -125,6 +131,8 @@ function sshHostToHost(h: SSHHostWithStatus): Host { tags: h.tags ?? [], authType: h.authType, password: h.password, + hasPassword: !!(h as any).hasPassword || !!h.password, + hasKey: !!(h as any).hasKey || !!(typeof h.key === "string" && h.key), key: typeof h.key === "string" ? h.key : undefined, keyPassword: h.keyPassword, keyType: h.keyType, @@ -132,15 +140,10 @@ function sshHostToHost(h: SSHHostWithStatus): Host { notes: h.notes, pin: h.pin ?? false, macAddress: h.macAddress, - enableSsh: - h.enableSsh != null - ? h.enableSsh - : h.connectionType === "ssh" || !h.connectionType, + enableSsh: h.enableSsh != null ? h.enableSsh : h.connectionType === "ssh", enableTerminal: h.enableTerminal ?? - (h.enableSsh != null - ? h.enableSsh - : h.connectionType === "ssh" || !h.connectionType), + (h.enableSsh != null ? h.enableSsh : h.connectionType === "ssh"), enableTunnel: h.enableTunnel ?? false, enableFileManager: h.enableFileManager ?? false, enableDocker: h.enableDocker ?? false, @@ -539,7 +542,7 @@ function HostRow({ paddingRight: "8px", }} > - {host.enableTerminal && ( + {host.enableSsh && host.enableTerminal && ( )} - {host.enableFileManager && ( + {host.enableSsh && host.enableFileManager && ( )} - {host.enableDocker && ( + {host.enableSsh && host.enableDocker && ( )} - {host.enableTunnel && ( + {host.enableSsh && host.enableTunnel && ( )} - {metricsEnabled && ( + {host.enableSsh && metricsEnabled && ( + ))} {host.enableSsh && - getSshActions(host).map(({ type, icon: Icon, label }) => ( - - ))} - {host.enableSsh && - (host.enableRdp || host.enableVnc || host.enableTelnet) && ( + (host.enableRdp || host.enableVnc || host.enableTelnet) && + getSshActions(host).length > 0 && (
)} {host.enableRdp && ( diff --git a/src/ui/ssh/dialogs/WarpgateDialog.tsx b/src/ui/ssh/dialogs/WarpgateDialog.tsx index 73337bcf..81e8a45a 100644 --- a/src/ui/ssh/dialogs/WarpgateDialog.tsx +++ b/src/ui/ssh/dialogs/WarpgateDialog.tsx @@ -41,12 +41,12 @@ export function WarpgateDialog({ }; return ( -
+
-
+
@@ -95,7 +95,7 @@ export function WarpgateDialog({
-
+