From ec69c338da311dfaded9778593e844fc2dbc64ae Mon Sep 17 00:00:00 2001 From: LukeGus Date: Wed, 20 May 2026 13:58:15 -0500 Subject: [PATCH] feat: host manager improvements and added over electron files --- src/main.tsx | 3 + src/ui/AppShell.tsx | 136 +++++++------ src/ui/auth/ElectronLoginForm.tsx | 8 +- src/ui/auth/ElectronServerConfig.tsx | 199 ++++++++----------- src/ui/dashboard/DashboardTab.tsx | 61 ++++-- src/ui/features/terminal/TerminalPreview.tsx | 121 +++++++++++ src/ui/locales/en.json | 14 +- src/ui/sidebar/AppRail.tsx | 2 +- src/ui/sidebar/HostManager.tsx | 197 ++++++++++++------ src/ui/sidebar/SidebarTree.tsx | 8 +- src/ui/sidebar/UserProfilePanel.tsx | 15 ++ src/ui/user/ElectronVersionCheck.tsx | 121 +++-------- 12 files changed, 535 insertions(+), 350 deletions(-) create mode 100644 src/ui/features/terminal/TerminalPreview.tsx diff --git a/src/main.tsx b/src/main.tsx index c9cb6666..25ee7368 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -143,6 +143,9 @@ function App() { setAuthUsername(u); setPhase("fading-in"); timerRef.current = setTimeout(() => setPhase("idle-app"), 450); + if (isElectron()) { + window.electronAPI?.startC2SAutoStartTunnels?.().catch(() => {}); + } } function handleLogout() { diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx index 3b1ddc84..9bc83199 100644 --- a/src/ui/AppShell.tsx +++ b/src/ui/AppShell.tsx @@ -288,48 +288,42 @@ export function AppShell({ }; window.addEventListener("termix:open-tab", handle); return () => window.removeEventListener("termix:open-tab", handle); - }, [tabs, allHosts]); + }, [allHosts]); // ─── 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 same = prev.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, - }; - setActiveTabId(tab.id); - return [...prev, tab]; - } const next = prev.map((t) => t.id === same[0].id && !/\(\d+\)$/.test(t.label) ? { ...t, label: `${host.name} (1)`, host } : t, ); - 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, - }; - setActiveTabId(tab.id); return [...next, tab]; }); + setActiveTabId(tab.id); } function connectHost(host: Host, preferredType?: TabType) { @@ -389,16 +383,26 @@ export function AppShell({ function doCloseTab(id: string) { terminalRefs.current.delete(id); + if (id === activeTabId) { + const remaining = tabs.filter((t) => t.id !== id); + setActiveTabId( + remaining.length > 0 ? remaining[remaining.length - 1].id : "dashboard", + ); + } setTabs((prev) => { const next = prev.filter((t) => t.id !== id); - if (id === activeTabId) setActiveTabId(next[next.length - 1].id); + if (next.length === 0) + return [ + { id: "dashboard", type: "dashboard", label: t("nav.dashboard") }, + ]; return next; }); } function closeTab(id: string) { const tab = tabs.find((t) => t.id === id); - if (tab && SESSION_TAB_TYPES.includes(tab.type)) { + const confirmEnabled = localStorage.getItem("confirmTabClose") === "true"; + if (tab && SESSION_TAB_TYPES.includes(tab.type) && confirmEnabled) { toast(t("nav.confirmClose"), { duration: 5000, action: { @@ -670,31 +674,37 @@ export function AppShell({ ) : ( <> {/* Terminal tabs: always in DOM, absolutely positioned so xterm always has real dimensions */} - {tabs - .filter((tab) => tab.type === "terminal") - .map((tab) => { - const visible = tab.id === activeTabId; - return ( -
- {renderTabContent( - tab, - openSingletonTab, - openTab, - closeTab, - visible, - )} -
- ); - })} - {/* Non-terminal tabs: display:none when inactive */} + {(() => { + 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, + )} +
+ ); + }); + })()} + {/* Non-terminal tabs: absolutely positioned above terminals when active */} {tabs .filter((tab) => tab.type !== "terminal") .map((tab) => { @@ -702,8 +712,16 @@ export function AppShell({ return (
{renderTabContent( tab, diff --git a/src/ui/auth/ElectronLoginForm.tsx b/src/ui/auth/ElectronLoginForm.tsx index e6785340..9824a30f 100644 --- a/src/ui/auth/ElectronLoginForm.tsx +++ b/src/ui/auth/ElectronLoginForm.tsx @@ -125,15 +125,15 @@ export function ElectronLoginForm({ const isEmbeddedServer = serverUrl.includes("localhost:30001"); return ( -
+
{isAuthenticating && ( -
+
)} {!isAuthenticating && ( -
+
-

- {t("serverConfig.embeddedDesc")} -

-
- )} - - {embeddedAvailable !== false && ( + {embeddedAvailable !== false && ( + <> + +

+ {t("serverConfig.embeddedDesc")} +

@@ -237,63 +208,61 @@ export function ElectronServerConfig({
+ + )} + +
+
+ + handleUrlChange(e.target.value)} + disabled={loading || embeddedLoading} + /> +
+ + {error && ( + + {t("common.error")} + {error} + )} -
-
- - handleUrlChange(e.target.value)} - className="w-full h-10" - disabled={loading || embeddedLoading} - /> -
- - {error && ( - - {t("common.error")} - {error} - - )} - -
- {onCancel && !isFirstTime && ( - - )} +
+ {onCancel && !isFirstTime && ( -
- -
- {t("serverConfig.helpText")} -
+ )} +
+ +

+ {t("serverConfig.helpText")} +

diff --git a/src/ui/dashboard/DashboardTab.tsx b/src/ui/dashboard/DashboardTab.tsx index cadd4c56..5a72051f 100644 --- a/src/ui/dashboard/DashboardTab.tsx +++ b/src/ui/dashboard/DashboardTab.tsx @@ -35,6 +35,7 @@ import { getServerMetricsById, registerMetricsViewer, sendMetricsHeartbeat, + getUserInfo, } from "@/main-axios"; import type { RecentActivityItem, SSHHostWithStatus } from "@/main-axios"; import { useTranslation } from "react-i18next"; @@ -278,10 +279,12 @@ function QuickActionsCard({ onOpenSingletonTab, hosts, onOpenTab, + isAdmin, }: { onOpenSingletonTab: (type: TabType, pendingEvent?: string) => void; hosts: Host[]; onOpenTab: (host: Host, type: TabType) => void; + isAdmin: boolean; }) { const { t } = useTranslation(); const pinnedHosts = hosts.filter((h) => h.pin); @@ -357,22 +360,24 @@ function QuickActionsCard({
) : ( <> - + {isAdmin && ( + + )}
))} @@ -1039,6 +1054,7 @@ export function DashboardTab({ }, [mainWidthPct]); const [hosts, setHosts] = useState([]); + const [isAdmin, setIsAdmin] = useState(false); const [uptimeFormatted, setUptimeFormatted] = useState(""); const [versionText, setVersionText] = useState(""); const [versionStatus, setVersionStatus] = useState< @@ -1112,6 +1128,9 @@ export function DashboardTab({ }; load(); + getUserInfo() + .then((info) => setIsAdmin(!!info.is_admin)) + .catch(() => {}); getUptime() .then((u) => setUptimeFormatted(u.formatted)) .catch(() => {}); @@ -1312,6 +1331,7 @@ export function DashboardTab({ onOpenSingletonTab, onOpenTab, cardLabels, + isAdmin, }; const isMobile = useIsMobile(); @@ -1395,13 +1415,18 @@ export function DashboardTab({ /> )} {slot.id === "quick_actions" && ( - + )} {slot.id === "host_status" && ( )} {slot.id === "recent_activity" && ( diff --git a/src/ui/features/terminal/TerminalPreview.tsx b/src/ui/features/terminal/TerminalPreview.tsx new file mode 100644 index 00000000..5a88d3ed --- /dev/null +++ b/src/ui/features/terminal/TerminalPreview.tsx @@ -0,0 +1,121 @@ +import { useTheme } from "@/components/theme-provider"; +import { TERMINAL_THEMES, TERMINAL_FONTS } from "@/lib/terminal-themes"; + +interface TerminalPreviewProps { + theme: string; + fontSize?: number; + fontFamily?: string; + cursorStyle?: "block" | "underline" | "bar"; + cursorBlink?: boolean; + letterSpacing?: number; + lineHeight?: number; +} + +export function TerminalPreview({ + theme = "termix", + fontSize = 14, + fontFamily = "Caskaydia Cove Nerd Font Mono", + cursorStyle = "bar", + cursorBlink = true, + letterSpacing = 0, + lineHeight = 1.0, +}: TerminalPreviewProps) { + const { theme: appTheme } = useTheme(); + + const resolvedTheme = + theme === "termix" + ? appTheme === "dark" || + (appTheme === "system" && + window.matchMedia("(prefers-color-scheme: dark)").matches) + ? "termixDark" + : "termixLight" + : theme; + + const colors = TERMINAL_THEMES[resolvedTheme]?.colors; + const fontFallback = + TERMINAL_FONTS.find((f) => f.value === fontFamily)?.fallback || + TERMINAL_FONTS[0].fallback; + + return ( +
+
+
+ deploy@web-01 + : + ~ + $ + ls -la +
+
total 48
+
+ drwxr-xr-x + + {" "} + 5 deploy deploy 4096 May 1 09:12{" "} + + . +
+
+ drwxr-xr-x + + {" "} + 3 root root 4096 Apr 15 18:44{" "} + + .. +
+
+ -rw-r--r-- + + {" "} + 1 deploy deploy 220 Apr 15 18:44{" "} + + .bash_logout +
+
+ -rwxr-xr-x + + {" "} + 1 deploy deploy 8192 May 1 08:55{" "} + + deploy.sh +
+
+ deploy@web-01 + : + ~ + $ + + +
+
+ +
+ ); +} diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index 8be051d5..f990f4b0 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -137,7 +137,9 @@ "history": "History", "hosts": "Hosts", "snippets": "Snippets", - "hostManager": "Host Manager" + "hostManager": "Host Manager", + "roleAdministrator": "Administrator", + "roleUser": "User" }, "hosts": { "hosts": "Hosts", @@ -459,7 +461,7 @@ "addTagsPlaceholder": "Add tags...", "authDetails": "Authentication Details", "credType": "Type", - "generateKeyPairDesc": "Generate a new key pair — both private and public keys will be filled automatically.", + "generateKeyPairDesc": "Generate a new key pair, both private and public keys will be filled automatically.", "generatingKey": "Generating...", "generateLabel": "Generate {{label}}", "uploadFileBtn": "Upload file", @@ -490,7 +492,7 @@ "connectionLabel": "Connection", "authenticationLabel": "Authentication", "generateKeyPairTitle": "Generate Key Pair", - "generateKeyPairDescription": "Generate a new key pair — both private and public keys will be filled automatically.", + "generateKeyPairDescription": "Generate a new key pair, both private and public keys will be filled automatically.", "generateFromPrivateKey": "Generate from Private Key", "refreshBtn2": "Refresh", "exitSelectionTitle": "Exit selection", @@ -690,7 +692,7 @@ "toggleWith": "Toggle with" }, "splitScreen": { - "paneEmpty": "Pane {{index}} — empty", + "paneEmpty": "Pane {{index}} - empty", "noTabAssigned": "No tab assigned" }, "guacamole": { @@ -1396,7 +1398,7 @@ "tunnel": "Tunnel", "docker": "Docker", "serverStats": "Server Stats", - "noNodes": "No nodes yet — add hosts to build your topology" + "noNodes": "No nodes yet" }, "docker": { "notEnabled": "Docker is not enabled for this host", @@ -1646,6 +1648,8 @@ "commandPaletteDesc": "Enable keyboard shortcut", "sessionPersistence": "Session Persistence", "sessionPersistenceDesc": "Keep sessions between reconnects", + "confirmTabClose": "Confirm Tab Close", + "confirmTabCloseDesc": "Ask before closing terminal tabs", "settingsSidebar": "Sidebar", "showHostTags": "Show Host Tags", "showHostTagsDesc": "Display tags in host list", diff --git a/src/ui/sidebar/AppRail.tsx b/src/ui/sidebar/AppRail.tsx index 307cbec9..efef227e 100644 --- a/src/ui/sidebar/AppRail.tsx +++ b/src/ui/sidebar/AppRail.tsx @@ -209,7 +209,7 @@ export function AppRail({ {username || "User"} - Administrator + {isAdmin ? t("nav.roleAdministrator") : t("nav.roleUser")}
diff --git a/src/ui/sidebar/HostManager.tsx b/src/ui/sidebar/HostManager.tsx index 2329351b..7ab38a69 100644 --- a/src/ui/sidebar/HostManager.tsx +++ b/src/ui/sidebar/HostManager.tsx @@ -64,6 +64,7 @@ import { } from "@/components/dropdown-menu"; import { toast } from "sonner"; import { SectionCard, SettingRow, FakeSwitch } from "@/components/section-card"; +import { TerminalPreview } from "@/features/terminal/TerminalPreview"; import { getSSHHosts, getCredentials, @@ -2047,58 +2048,15 @@ function HostEditor({ -
-
-
- deploy@web-01 - : - ~ - $ - ls -la -
-
total 48
-
- drwxr-xr-x - - {" "} - 5 deploy deploy 4096 May 1 09:12{" "} - - . -
-
- drwxr-xr-x - - {" "} - 3 root root 4096 Apr 15 18:44{" "} - - .. -
-
- -rw-r--r-- - - {" "} - 1 deploy deploy 220 Apr 15 18:44{" "} - - .bash_logout -
-
- -rwxr-xr-x - - {" "} - 1 deploy deploy 8192 May 1 08:55{" "} - - deploy.sh -
-
- deploy@web-01 - : - ~ - $ - - -
-
-
+
@@ -5506,19 +5464,146 @@ export function HostManager({ { hosts: [ { - name: "My Server", - ip: "192.168.1.1", - username: "root", - port: 22, + name: "Web Server (Production)", + ip: "192.168.1.100", + username: "admin", + authType: "password", + password: "your_secure_password_here", folder: "Production", + tags: ["web", "production", "nginx"], + pin: true, + notes: "Main production web server running Nginx", enableSsh: true, enableRdp: false, enableVnc: false, enableTelnet: false, sshPort: 22, + enableTerminal: true, + enableTunnel: false, + enableFileManager: true, + enableDocker: false, + defaultPath: "/var/www", + }, + { + name: "Database Server", + ip: "192.168.1.101", + username: "dbadmin", + authType: "key", + key: "-----BEGIN OPENSSH PRIVATE KEY-----\nYour SSH private key content here\n-----END OPENSSH PRIVATE KEY-----", + keyPassword: "optional_key_passphrase", + keyType: "ssh-ed25519", + folder: "Production", + tags: ["database", "production", "postgresql"], + enableSsh: true, + enableRdp: false, + enableVnc: false, + enableTelnet: false, + sshPort: 22, + enableTerminal: true, + enableTunnel: true, + enableFileManager: false, + enableDocker: false, + tunnelConnections: [ + { + sourcePort: 5432, + endpointPort: 5432, + endpointHost: "localhost", + maxRetries: 3, + retryInterval: 10, + autoStart: true, + }, + ], + statsConfig: { + enabledWidgets: ["cpu", "memory", "disk", "network", "uptime"], + statusCheckEnabled: true, + statusCheckInterval: 30, + metricsEnabled: true, + metricsInterval: 30, + }, + }, + { + name: "Development Server", + ip: "192.168.1.102", + username: "developer", + authType: "password", + password: "dev_password", + folder: "Development", + tags: ["dev", "testing"], + enableSsh: true, + enableRdp: false, + enableVnc: false, + enableTelnet: false, + sshPort: 2222, + enableTerminal: true, + enableTunnel: false, + enableFileManager: true, + enableDocker: true, + defaultPath: "/home/developer", + }, + { + name: "Windows Server 2022", + ip: "192.168.1.200", + username: "Administrator", + folder: "Remote Desktop", + tags: ["rdp", "windows", "production"], + enableSsh: false, + enableRdp: true, + enableVnc: false, + enableTelnet: false, rdpPort: 3389, + rdpUser: "Administrator", + rdpPassword: "windows_password", + rdpDomain: "COMPANY", + rdpSecurity: "nla", + rdpIgnoreCert: false, + }, + { + name: "Ubuntu Desktop", + ip: "192.168.1.201", + username: "vncuser", + folder: "Remote Desktop", + tags: ["vnc", "linux", "desktop"], + enableSsh: false, + enableRdp: false, + enableVnc: true, + enableTelnet: false, vncPort: 5900, + vncPassword: "vnc_password", + }, + { + name: "Network Switch", + ip: "192.168.1.254", + username: "admin", + folder: "Infrastructure", + tags: ["telnet", "network", "switch"], + enableSsh: false, + enableRdp: false, + enableVnc: false, + enableTelnet: true, telnetPort: 23, + telnetUser: "admin", + telnetPassword: "switch_password", + }, + { + name: "Server with SOCKS5 Proxy", + ip: "10.10.10.100", + username: "proxyuser", + authType: "password", + password: "secure_password", + folder: "Proxied Hosts", + tags: ["proxy", "socks5"], + enableSsh: true, + enableRdp: false, + enableVnc: false, + enableTelnet: false, + sshPort: 22, + enableTerminal: true, + enableFileManager: true, + useSocks5: true, + socks5Host: "proxy.example.com", + socks5Port: 1080, + socks5Username: "proxyauth", + socks5Password: "proxypass", }, ], }, diff --git a/src/ui/sidebar/SidebarTree.tsx b/src/ui/sidebar/SidebarTree.tsx index 9daa3b72..18eea1f2 100644 --- a/src/ui/sidebar/SidebarTree.tsx +++ b/src/ui/sidebar/SidebarTree.tsx @@ -139,7 +139,13 @@ export function HostItem({ className={`relative flex items-stretch cursor-pointer select-none transition-colors hover:bg-muted/40 ${stripeIndex % 2 === 1 ? "bg-muted/20" : ""}`} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} - onClick={() => onOpenTab("terminal")} + onClick={() => { + if (host.enableSsh) onOpenTab("terminal"); + else if (host.enableRdp) onOpenTab("rdp"); + else if (host.enableVnc) onOpenTab("vnc"); + else if (host.enableTelnet) onOpenTab("telnet"); + else onOpenTab("terminal"); + }} > {/* Status stripe */}
localStorage.getItem("disableUpdateCheck") === "true", ); + const [confirmTabClose, setConfirmTabClose] = useState( + () => localStorage.getItem("confirmTabClose") === "true", + ); // API keys const [apiKeys, setApiKeys] = useState([]); @@ -1008,6 +1011,18 @@ export function UserProfilePanel({ }} /> + + { + setConfirmTabClose(v); + localStorage.setItem("confirmTabClose", v.toString()); + }} + /> +
diff --git a/src/ui/user/ElectronVersionCheck.tsx b/src/ui/user/ElectronVersionCheck.tsx index 57cb78b5..0cb489f1 100644 --- a/src/ui/user/ElectronVersionCheck.tsx +++ b/src/ui/user/ElectronVersionCheck.tsx @@ -4,7 +4,6 @@ import { VersionAlert } from "@/components/version-alert.tsx"; import { useTranslation } from "react-i18next"; import { isElectron } from "@/lib/electron"; import { checkElectronUpdate } from "@/main-axios.ts"; -import { useTheme } from "@/components/theme-provider"; interface VersionCheckModalProps { onContinue: () => void; @@ -18,7 +17,6 @@ type ElectronWindow = Window & { export function ElectronVersionCheck({ onContinue }: VersionCheckModalProps) { const { t } = useTranslation(); - const { theme } = useTheme(); const [versionInfo, setVersionInfo] = useState -
-
-
-
-

+

+
+
+

{t("versionCheck.checkingUpdates")}

@@ -139,76 +114,40 @@ export function ElectronVersionCheck({ onContinue }: VersionCheckModalProps) { if (!versionInfo || versionDismissed) { return ( -
-
-
-

- {t("versionCheck.checkUpdates")} -

-
- +
+
+

{t("versionCheck.checkUpdates")}

{versionInfo && !versionDismissed && ( -
- -
+ )} - -
- -
+
); } return ( -
-
-
-

{versionModalTitle}

-
- -
- -
- -
- -
+
+
+

{versionModalTitle}

+ +
);