diff --git a/src/main.tsx b/src/main.tsx index a83f3ade..cfab33d2 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -12,6 +12,7 @@ import { getUserInfo, getCurrentToken, appReadyPromise } from "@/main-axios"; import { applyAccentColor, applyFontSize } from "@/lib/theme"; import type { FontSizeId } from "@/types/ui-types"; import { useServiceWorker } from "@/hooks/use-service-worker"; +import { useTranslation } from "react-i18next"; const AppShell = lazy(() => import("@/AppShell").then((m) => ({ default: m.AppShell })), @@ -177,19 +178,36 @@ function App() { const appOpacity = phase === "idle-app" ? 1 : 0; const authOpacity = phase === "idle-auth" ? 1 : 0; + const { t } = useTranslation(); + const isTransitioning = phase === "fading-in" || phase === "fading-out"; + if (phase === "verifying") { return (
-
+
+
+

{t("common.loading")}

+
); } return ( <> + {isTransitioning && ( +
+
+
+

+ {t("common.loading")} +

+
+
+ )} + {showApp && (
void; + reconnect?: () => void; } | null>; }; diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx index 15534e1b..6d7a2289 100644 --- a/src/ui/AppShell.tsx +++ b/src/ui/AppShell.tsx @@ -398,6 +398,19 @@ export function AppShell({ }); } + function refreshTab(id: string) { + const tab = tabs.find((t) => t.id === id); + if (!tab) return; + if (tab.type === "terminal") { + const ref = tab.terminalRef?.current; + ref?.reconnect?.(); + } else if (["rdp", "vnc", "telnet"].includes(tab.type)) { + window.dispatchEvent( + new CustomEvent("termix:refresh-guacamole", { detail: { tabId: id } }), + ); + } + } + function closeTab(id: string) { const tab = tabs.find((t) => t.id === id); const confirmEnabled = localStorage.getItem("confirmTabClose") === "true"; @@ -556,7 +569,7 @@ export function AppShell({ -
- ) : ( - - )} +
); }); diff --git a/src/ui/features/guacamole/GuacamoleToolbar.tsx b/src/ui/features/guacamole/GuacamoleToolbar.tsx index 103dcb36..f81ed4a3 100644 --- a/src/ui/features/guacamole/GuacamoleToolbar.tsx +++ b/src/ui/features/guacamole/GuacamoleToolbar.tsx @@ -8,7 +8,6 @@ import React, { import { GripVertical, Monitor, - RefreshCw, ChevronLeft, ChevronRight, ChevronUp, @@ -240,247 +239,239 @@ export const GuacamoleToolbar: React.FC = ({ zIndex: 20, }; - if (collapsed) { - return ( -
e.preventDefault()} - > - - -
- -
- -
- - - {t("guacamole.toolbar.expand")} - - -
- ); - } - return (
e.preventDefault()} - className="flex items-center bg-background/85 backdrop-blur-sm border border-border shadow-lg rounded-sm px-0.5 py-0.5 gap-0" > - {/* Drag handle */} - - - - - - {t("guacamole.toolbar.dragHandle")} - - + {collapsed ? ( + + +
+ +
+ +
+ + + {t("guacamole.toolbar.expand")} + + + ) : ( +
+ {/* Drag handle */} + + + + + + {t("guacamole.toolbar.dragHandle")} + + - {/* System combos — RDP/VNC only */} - {isRdpVnc && ( - <> + {/* System combos — RDP/VNC only */} + {isRdpVnc && ( + <> +
+ sendCombo(0xffe3, 0xffe9, 0xffff)} + > + CAD + + sendCombo(0xff67, 0x006c)} + > + Win+L + + sendCombo(0xff67)} + > + Win + + + )} + + {/* Sticky modifiers — RDP/VNC only */} + {isRdpVnc && ( + <> +
+ {( + [ + [ + "ctrl", + MODIFIER_KEYSYMS.ctrl, + t("guacamole.toolbar.ctrl"), + ], + ["alt", MODIFIER_KEYSYMS.alt, t("guacamole.toolbar.alt")], + [ + "shift", + MODIFIER_KEYSYMS.shift, + t("guacamole.toolbar.shift"), + ], + ["win", MODIFIER_KEYSYMS.win, t("guacamole.toolbar.win")], + ] as [string, number, string][] + ).map(([key, ks, label]) => ( + + + + + + {stickyKeys[ks] + ? t("guacamole.toolbar.stickyActive", { key: label }) + : t("guacamole.toolbar.stickyInactive", { key: label })} + + + ))} + + )} + + {/* Function key toggle */}
sendCombo(0xffe3, 0xffe9, 0xffff)} + tooltip={t("guacamole.toolbar.fnToggle")} + onClick={() => setShowFKeys((v) => !v)} + className={cn( + showFKeys && + "bg-primary/15 text-primary border border-primary/30", + )} > - CAD + Fn - sendCombo(0xff67, 0x006c)} - > - Win+L - - sendCombo(0xff67)} - > - Win - - - )} - {/* Sticky modifiers — RDP/VNC only */} - {isRdpVnc && ( - <> + {/* F1-F12 row */} + {showFKeys && + FKEY_KEYSYMS.map((ks, i) => ( + sendCombo(ks)} + > + F{i + 1} + + ))} + + {/* Navigation */}
- {( - [ - ["ctrl", MODIFIER_KEYSYMS.ctrl, t("guacamole.toolbar.ctrl")], - ["alt", MODIFIER_KEYSYMS.alt, t("guacamole.toolbar.alt")], - ["shift", MODIFIER_KEYSYMS.shift, t("guacamole.toolbar.shift")], - ["win", MODIFIER_KEYSYMS.win, t("guacamole.toolbar.win")], - ] as [string, number, string][] - ).map(([key, ks, label]) => ( - - - - - - {stickyKeys[ks] - ? t("guacamole.toolbar.stickyActive", { key: label }) - : t("guacamole.toolbar.stickyInactive", { key: label })} - - - ))} - - )} - - {/* Function key toggle */} -
- setShowFKeys((v) => !v)} - className={cn( - showFKeys && "bg-primary/15 text-primary border border-primary/30", - )} - > - Fn - - - {/* F1-F12 row */} - {showFKeys && - FKEY_KEYSYMS.map((ks, i) => ( sendCombo(ks)} + tooltip={t("guacamole.toolbar.esc")} + onClick={() => sendCombo(0xff1b)} > - F{i + 1} + Esc - ))} - - {/* Navigation */} -
- sendCombo(0xff1b)} - > - Esc - - sendCombo(0xff09)} - > - Tab - - sendCombo(0xff50)} - > - Home - - sendCombo(0xff57)} - > - End - - sendCombo(0xff55)} - > - PgUp - - sendCombo(0xff56)} - > - PgDn - - - {/* Arrow cluster */} -
-
- sendCombo(0xff52)} + sendCombo(0xff09)} > - - + Tab + + sendCombo(0xff50)} + > + Home + + sendCombo(0xff57)} + > + End + + sendCombo(0xff55)} + > + PgUp + + sendCombo(0xff56)} + > + PgDn + + + {/* Arrow cluster */} +
+
+ sendCombo(0xff52)} + > + + +
+
+ sendCombo(0xff51)} + > + + + sendCombo(0xff54)} + > + + + sendCombo(0xff53)} + > + + +
+
+ + {/* Collapse */} +
+ + + + + + {t("guacamole.toolbar.collapse")} + +
-
- sendCombo(0xff51)} - > - - - sendCombo(0xff54)} - > - - - sendCombo(0xff53)} - > - - -
-
- - {/* Session */} -
- - - - - {/* Collapse */} -
- - - - - - {t("guacamole.toolbar.collapse")} - - + )}
); diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index ee172558..fbe9eb3f 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -131,6 +131,7 @@ "passwordCopied": "Password copied to clipboard", "noPasswordAvailable": "No password available", "failedToCopyPassword": "Failed to copy password", + "refreshTab": "Refresh connection", "openFileManager": "Open File Manager", "dashboard": "Dashboard", "networkGraph": "Network Graph", @@ -293,6 +294,9 @@ "keyFileLoaded": "Key file loaded", "keyUploadClick": "Click to upload .pem / .key / .ppk", "clearKey": "Clear key", + "keySaved": "SSH key saved", + "keyReplaceNotice": "paste a new key below to replace it", + "replaceKey": "Replace key", "forceKeyboardInteractiveLabel": "Force Keyboard Interactive", "forceKeyboardInteractiveShortDesc": "Force manual password entry even if keys are present", "terminalAppearance": "Terminal Appearance", @@ -509,6 +513,7 @@ "hostsTab": "Hosts", "credentialsTab": "Credentials", "selectMultiple": "Select multiple", + "selectHosts": "Select hosts", "connectionLabel": "Connection", "authenticationLabel": "Authentication", "generateKeyPairTitle": "Generate Key Pair", diff --git a/src/ui/shell/TabBar.tsx b/src/ui/shell/TabBar.tsx index 56b6b90f..c868b2ff 100644 --- a/src/ui/shell/TabBar.tsx +++ b/src/ui/shell/TabBar.tsx @@ -6,21 +6,25 @@ import { DropdownMenuContent, DropdownMenuTrigger, } from "@/components/dropdown-menu"; -import { ChevronDown, ChevronUp, X } from "lucide-react"; +import { ChevronDown, ChevronUp, RefreshCw, X } from "lucide-react"; import { tabIcon } from "@/shell/tabUtils"; -import type { Tab } from "@/types/ui-types"; +import type { Tab, TabType } from "@/types/ui-types"; + +const CONNECTION_TAB_TYPES: TabType[] = ["terminal", "rdp", "vnc", "telnet"]; export function TabBar({ tabs, activeTabId, onSetActiveTab, onCloseTab, + onRefreshTab, onReorderTabs, }: { tabs: Tab[]; activeTabId: string; onSetActiveTab: (id: string) => void; onCloseTab: (id: string) => void; + onRefreshTab: (id: string) => void; onReorderTabs: (tabs: Tab[]) => void; }) { const [open, setOpen] = useState(true); @@ -217,16 +221,33 @@ export function TabBar({ {tabIcon(tab.type)} {tab.type !== "dashboard" && tab.label} {tab.type !== "dashboard" && ( - + {CONNECTION_TAB_TYPES.includes(tab.type) && ( + + )} + +
)}
); @@ -272,7 +293,7 @@ export function TabBar({ @@ -313,7 +334,7 @@ export function TabBar({ diff --git a/src/ui/shell/tabUtils.tsx b/src/ui/shell/tabUtils.tsx index d96f5f7e..a5a0d906 100644 --- a/src/ui/shell/tabUtils.tsx +++ b/src/ui/shell/tabUtils.tsx @@ -229,7 +229,7 @@ export function renderTabContent( return ( ); - return ; + return ; case "network_graph": return ; diff --git a/src/ui/sidebar/HostManager.tsx b/src/ui/sidebar/HostManager.tsx index e81014a4..79575bb9 100644 --- a/src/ui/sidebar/HostManager.tsx +++ b/src/ui/sidebar/HostManager.tsx @@ -159,14 +159,20 @@ function sshHostToHost(h: SSHHostWithStatus): Host { vncPort: h.vncPort ?? (h.connectionType === "vnc" ? h.port : 5900), telnetPort: h.telnetPort ?? (h.connectionType === "telnet" ? h.port : 23), rdpUser: h.rdpUser, - rdpPassword: h.rdpPassword, + rdpPassword: (h as any).hasRdpPassword + ? "existing_password" + : (h.rdpPassword ?? ""), domain: h.rdpDomain, security: h.rdpSecurity, ignoreCert: h.rdpIgnoreCert ?? false, - vncPassword: h.vncPassword, + vncPassword: (h as any).hasVncPassword + ? "existing_password" + : (h.vncPassword ?? ""), vncUser: h.vncUser, telnetUser: h.telnetUser, - telnetPassword: h.telnetPassword, + telnetPassword: (h as any).hasTelnetPassword + ? "existing_password" + : (h.telnetPassword ?? ""), quickActions: (h.quickActions ?? []).map((a: any) => ({ name: a.name, snippetId: String(a.snippetId), @@ -945,14 +951,20 @@ function HostEditor({ quickActions: host?.quickActions ?? ([] as { name: string; snippetId: string }[]), rdpUser: host?.rdpUser ?? "", - rdpPassword: host?.rdpPassword ?? "", + rdpPassword: (host as any)?.hasRdpPassword + ? "existing_password" + : (host?.rdpPassword ?? ""), domain: host?.domain ?? "", security: host?.security ?? "", ignoreCert: host?.ignoreCert ?? false, - vncPassword: host?.vncPassword ?? "", + vncPassword: (host as any)?.hasVncPassword + ? "existing_password" + : (host?.vncPassword ?? ""), vncUser: host?.vncUser ?? "", telnetUser: host?.telnetUser ?? "", - telnetPassword: host?.telnetPassword ?? "", + telnetPassword: (host as any)?.hasTelnetPassword + ? "existing_password" + : (host?.telnetPassword ?? ""), guacamoleConfig: host?.guacamoleConfig ?? ({} as Record), statsConfig: host?.statsConfig ?? { statusCheckEnabled: true, @@ -1121,14 +1133,23 @@ function HostEditor({ telnetPort: Number(form.telnetPort), forceKeyboardInteractive: form.forceKeyboardInteractive, rdpUser: form.rdpUser || null, - rdpPassword: form.rdpPassword || null, + rdpPassword: + form.rdpPassword === "existing_password" + ? undefined + : form.rdpPassword || null, rdpDomain: form.domain || null, rdpSecurity: form.security || null, rdpIgnoreCert: form.ignoreCert, - vncPassword: form.vncPassword || null, + vncPassword: + form.vncPassword === "existing_password" + ? undefined + : form.vncPassword || null, vncUser: form.vncUser || null, telnetUser: form.telnetUser || null, - telnetPassword: form.telnetPassword || null, + telnetPassword: + form.telnetPassword === "existing_password" + ? undefined + : form.telnetPassword || null, jumpHosts: form.jumpHosts, portKnockSequence: form.portKnockSequence, tunnelConnections: form.serverTunnels, @@ -1187,6 +1208,9 @@ function HostEditor({ }; const authMethod = form.authType; + const selectedCredential = credentials.find( + (c) => c.id === form.credentialId, + ); const handleProtocolToggle = ( proto: keyof typeof protocols, @@ -1934,6 +1958,11 @@ function HostEditor({ setField("username", e.target.value)} />
@@ -1985,13 +2014,23 @@ function HostEditor({
{form.keySubTab === "paste" ? ( -