diff --git a/src/backend/database/db/schema.ts b/src/backend/database/db/schema.ts index 074b4103..2bf276aa 100644 --- a/src/backend/database/db/schema.ts +++ b/src/backend/database/db/schema.ts @@ -86,8 +86,12 @@ export const sshData = sqliteTable("ssh_data", { enableFileManager: integer("enable_file_manager", { mode: "boolean" }) .notNull() .default(true), + enableDocker: integer("enable_docker", { mode: "boolean" }) + .notNull() + .default(false), defaultPath: text("default_path"), statsConfig: text("stats_config"), + dockerConfig: text("docker_config"), terminalConfig: text("terminal_config"), quickActions: text("quick_actions"), createdAt: text("created_at") diff --git a/src/backend/database/routes/ssh.ts b/src/backend/database/routes/ssh.ts index 955135a4..6cde8e29 100644 --- a/src/backend/database/routes/ssh.ts +++ b/src/backend/database/routes/ssh.ts @@ -235,11 +235,13 @@ router.post( enableTerminal, enableTunnel, enableFileManager, + enableDocker, defaultPath, tunnelConnections, jumpHosts, quickActions, statsConfig, + dockerConfig, terminalConfig, forceKeyboardInteractive, } = hostData; @@ -280,8 +282,10 @@ router.post( ? JSON.stringify(quickActions) : null, enableFileManager: enableFileManager ? 1 : 0, + enableDocker: enableDocker ? 1 : 0, defaultPath: defaultPath || null, statsConfig: statsConfig ? JSON.stringify(statsConfig) : null, + dockerConfig: dockerConfig ? JSON.stringify(dockerConfig) : null, terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null, forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false", }; @@ -341,9 +345,13 @@ router.post( ? JSON.parse(createdHost.jumpHosts as string) : [], enableFileManager: !!createdHost.enableFileManager, + enableDocker: !!createdHost.enableDocker, statsConfig: createdHost.statsConfig ? JSON.parse(createdHost.statsConfig as string) : undefined, + dockerConfig: createdHost.dockerConfig + ? JSON.parse(createdHost.dockerConfig as string) + : undefined, }; const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost; @@ -457,11 +465,13 @@ router.put( enableTerminal, enableTunnel, enableFileManager, + enableDocker, defaultPath, tunnelConnections, jumpHosts, quickActions, statsConfig, + dockerConfig, terminalConfig, forceKeyboardInteractive, } = hostData; @@ -503,8 +513,10 @@ router.put( ? JSON.stringify(quickActions) : null, enableFileManager: enableFileManager ? 1 : 0, + enableDocker: enableDocker ? 1 : 0, defaultPath: defaultPath || null, statsConfig: statsConfig ? JSON.stringify(statsConfig) : null, + dockerConfig: dockerConfig ? JSON.stringify(dockerConfig) : null, terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null, forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false", }; @@ -582,9 +594,13 @@ router.put( ? JSON.parse(updatedHost.jumpHosts as string) : [], enableFileManager: !!updatedHost.enableFileManager, + enableDocker: !!updatedHost.enableDocker, statsConfig: updatedHost.statsConfig ? JSON.parse(updatedHost.statsConfig as string) : undefined, + dockerConfig: updatedHost.dockerConfig + ? JSON.parse(updatedHost.dockerConfig as string) + : undefined, }; const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost; @@ -683,9 +699,13 @@ router.get( ? JSON.parse(row.quickActions as string) : [], enableFileManager: !!row.enableFileManager, + enableDocker: !!row.enableDocker, statsConfig: row.statsConfig ? JSON.parse(row.statsConfig as string) : undefined, + dockerConfig: row.dockerConfig + ? JSON.parse(row.dockerConfig as string) + : undefined, terminalConfig: row.terminalConfig ? JSON.parse(row.terminalConfig as string) : undefined, diff --git a/src/types/index.ts b/src/types/index.ts index 83dad26a..1970996a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -14,6 +14,18 @@ export interface QuickAction { snippetId: number; } +export interface DockerConfig { + connectionType: "socket" | "tcp" | "tls"; + socketPath?: string; + host?: string; + port?: number; + tlsVerify?: boolean; + tlsCaCert?: string; + tlsCert?: string; + tlsKey?: string; + apiVersion?: string; +} + export interface SSHHost { id: number; name: string; @@ -40,11 +52,13 @@ export interface SSHHost { enableTerminal: boolean; enableTunnel: boolean; enableFileManager: boolean; + enableDocker: boolean; defaultPath: string; tunnelConnections: TunnelConnection[]; jumpHosts?: JumpHost[]; quickActions?: QuickAction[]; statsConfig?: string; + dockerConfig?: string; terminalConfig?: TerminalConfig; createdAt: string; updatedAt: string; @@ -77,12 +91,14 @@ export interface SSHHostData { enableTerminal?: boolean; enableTunnel?: boolean; enableFileManager?: boolean; + enableDocker?: boolean; defaultPath?: string; forceKeyboardInteractive?: boolean; tunnelConnections?: TunnelConnection[]; jumpHosts?: JumpHostData[]; quickActions?: QuickActionData[]; statsConfig?: string | Record; + dockerConfig?: DockerConfig | string; terminalConfig?: TerminalConfig; } @@ -339,13 +355,14 @@ export interface TerminalConfig { export interface TabContextTab { id: number; type: - | "home" - | "terminal" - | "ssh_manager" - | "server" - | "admin" - | "file_manager" - | "user_profile"; + | "home" + | "terminal" + | "ssh_manager" + | "server" + | "admin" + | "file_manager" + | "user_profile" + | "docker"; title: string; hostConfig?: SSHHost; terminalRef?: any; diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx index fb015997..99cfa6d9 100644 --- a/src/ui/desktop/DesktopApp.tsx +++ b/src/ui/desktop/DesktopApp.tsx @@ -155,7 +155,9 @@ function AppContent() { const showTerminalView = currentTabData?.type === "terminal" || currentTabData?.type === "server" || - currentTabData?.type === "file_manager"; + currentTabData?.type === "file_manager" || + currentTabData?.type === "tunnel" || + currentTabData?.type === "docker"; const showHome = currentTabData?.type === "home"; const showSshManager = currentTabData?.type === "ssh_manager"; const showAdmin = currentTabData?.type === "admin"; diff --git a/src/ui/desktop/apps/docker/DockerManager.tsx b/src/ui/desktop/apps/docker/DockerManager.tsx new file mode 100644 index 00000000..1fc01acd --- /dev/null +++ b/src/ui/desktop/apps/docker/DockerManager.tsx @@ -0,0 +1,126 @@ +import React from "react"; +import { useSidebar } from "@/components/ui/sidebar.tsx"; +import { Separator } from "@/components/ui/separator.tsx"; +import { useTranslation } from "react-i18next"; + +interface HostConfig { + id: number; + name: string; + ip: string; + username: string; + folder?: string; + enableFileManager?: boolean; + tunnelConnections?: unknown[]; + [key: string]: unknown; +} + +interface DockerManagerProps { + hostConfig?: HostConfig; + title?: string; + isVisible?: boolean; + isTopbarOpen?: boolean; + embedded?: boolean; +} + +export function DockerManager({ + hostConfig, + title, + isVisible = true, + isTopbarOpen = true, + embedded = false, +}: DockerManagerProps): React.ReactElement { + const { t } = useTranslation(); + const { state: sidebarState } = useSidebar(); + const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig); + + React.useEffect(() => { + if (hostConfig?.id !== currentHostConfig?.id) { + setCurrentHostConfig(hostConfig); + } + }, [hostConfig?.id]); + + React.useEffect(() => { + const fetchLatestHostConfig = async () => { + if (hostConfig?.id) { + try { + const { getSSHHosts } = await import("@/ui/main-axios.ts"); + const hosts = await getSSHHosts(); + const updatedHost = hosts.find((h) => h.id === hostConfig.id); + if (updatedHost) { + setCurrentHostConfig(updatedHost); + } + } catch { + // Silently handle error + } + } + }; + + fetchLatestHostConfig(); + + const handleHostsChanged = async () => { + if (hostConfig?.id) { + try { + const { getSSHHosts } = await import("@/ui/main-axios.ts"); + const hosts = await getSSHHosts(); + const updatedHost = hosts.find((h) => h.id === hostConfig.id); + if (updatedHost) { + setCurrentHostConfig(updatedHost); + } + } catch { + // Silently handle error + } + } + }; + + window.addEventListener("ssh-hosts:changed", handleHostsChanged); + return () => + window.removeEventListener("ssh-hosts:changed", handleHostsChanged); + }, [hostConfig?.id]); + + const topMarginPx = isTopbarOpen ? 74 : 16; + const leftMarginPx = sidebarState === "collapsed" ? 16 : 8; + const bottomMarginPx = 8; + + const wrapperStyle: React.CSSProperties = embedded + ? { opacity: isVisible ? 1 : 0, height: "100%", width: "100%" } + : { + opacity: isVisible ? 1 : 0, + marginLeft: leftMarginPx, + marginRight: 17, + marginTop: topMarginPx, + marginBottom: bottomMarginPx, + height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`, + }; + + const containerClass = embedded + ? "h-full w-full text-white overflow-hidden bg-transparent" + : "bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden"; + + return ( +
+
+
+
+
+

+ {currentHostConfig?.folder} / {title} +

+
+
+
+ + +
+ {/* Empty body as requested */} +
+
+

+ Docker management UI will be here. +

+
+
+
+
+
+ ); +} diff --git a/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx b/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx index c1d595d2..7332323d 100644 --- a/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx +++ b/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx @@ -566,6 +566,20 @@ export function HostManagerEditor({ }), ) .default([]), + enableDocker: z.boolean().default(false), + dockerConfig: z + .object({ + connectionType: z.enum(["socket", "tcp", "tls"]).default("socket"), + socketPath: z.string().optional(), + host: z.string().optional(), + port: z.coerce.number().min(1).max(65535).optional(), + tlsVerify: z.boolean().default(true), + tlsCaCert: z.string().optional(), + tlsCert: z.string().optional(), + tlsKey: z.string().optional(), + apiVersion: z.string().optional(), + }) + .optional(), }) .superRefine((data, ctx) => { if (data.authType === "none") { @@ -658,6 +672,18 @@ export function HostManagerEditor({ statsConfig: DEFAULT_STATS_CONFIG, terminalConfig: DEFAULT_TERMINAL_CONFIG, forceKeyboardInteractive: false, + enableDocker: false, + dockerConfig: { + connectionType: "socket" as const, + socketPath: "/var/run/docker.sock", + host: "", + port: 2375, + tlsVerify: true, + tlsCaCert: "", + tlsCert: "", + tlsKey: "", + apiVersion: "", + }, }, }); @@ -753,6 +779,18 @@ export function HostManagerEditor({ : [], }, forceKeyboardInteractive: Boolean(cleanedHost.forceKeyboardInteractive), + enableDocker: Boolean(cleanedHost.enableDocker), + dockerConfig: cleanedHost.dockerConfig || { + connectionType: "socket" as const, + socketPath: "/var/run/docker.sock", + host: "", + port: 2375, + tlsVerify: true, + tlsCaCert: "", + tlsCert: "", + tlsKey: "", + apiVersion: "", + }, }; if (defaultAuthType === "password") { @@ -804,6 +842,18 @@ export function HostManagerEditor({ statsConfig: DEFAULT_STATS_CONFIG, terminalConfig: DEFAULT_TERMINAL_CONFIG, forceKeyboardInteractive: false, + enableDocker: false, + dockerConfig: { + connectionType: "socket" as const, + socketPath: "/var/run/docker.sock", + host: "", + port: 2375, + tlsVerify: true, + tlsCaCert: "", + tlsCert: "", + tlsKey: "", + apiVersion: "", + }, }; form.reset(defaultFormData); @@ -861,6 +911,8 @@ export function HostManagerEditor({ authType: data.authType, overrideCredentialUsername: Boolean(data.overrideCredentialUsername), enableTerminal: Boolean(data.enableTerminal), + enableDocker: Boolean(data.enableDocker), + dockerConfig: data.dockerConfig || null, enableTunnel: Boolean(data.enableTunnel), enableFileManager: Boolean(data.enableFileManager), defaultPath: data.defaultPath || "/", @@ -948,9 +1000,8 @@ export function HostManagerEditor({ window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); if (savedHost?.id) { - const { notifyHostCreatedOrUpdated } = await import( - "@/ui/main-axios.ts" - ); + const { notifyHostCreatedOrUpdated } = + await import("@/ui/main-axios.ts"); notifyHostCreatedOrUpdated(savedHost.id); } } catch (error) { @@ -983,6 +1034,8 @@ export function HostManagerEditor({ setActiveTab("general"); } else if (errors.enableTerminal || errors.terminalConfig) { setActiveTab("terminal"); + } else if (errors.enableDocker || errors.dockerConfig) { + setActiveTab("docker"); } else if (errors.enableTunnel || errors.tunnelConnections) { setActiveTab("tunnel"); } else if (errors.enableFileManager || errors.defaultPath) { @@ -1175,6 +1228,7 @@ export function HostManagerEditor({ {t("hosts.terminal")} + Docker {t("hosts.tunnel")} {t("hosts.fileManager")} @@ -2545,6 +2599,307 @@ export function HostManagerEditor({ + + ( + + Enable Docker + + + + + Enable Docker integration for this host + + + )} + /> + + {form.watch("enableDocker") && ( + <> + + + Docker Configuration +
+ Configure connection to Docker daemon on this host. + You can connect via Unix socket, TCP, or secure TLS + connection. +
+
+
+ + ( + + Connection Type + + + Choose how to connect to the Docker daemon + + + )} + /> + + {form.watch("dockerConfig.connectionType") === + "socket" && ( + ( + + Socket Path + + + + + Path to the Docker Unix socket (default: + /var/run/docker.sock) + + + )} + /> + )} + + {(form.watch("dockerConfig.connectionType") === "tcp" || + form.watch("dockerConfig.connectionType") === + "tls") && ( + <> +
+ ( + + Docker Host + + + + + )} + /> + + ( + + Port + + + + + )} + /> +
+ + )} + + {form.watch("dockerConfig.connectionType") === "tls" && ( + + + + TLS Configuration + + + ( + +
+ Verify TLS + + Verify the Docker daemon's certificate + +
+ + + +
+ )} + /> + + ( + + CA Certificate + + + + + Certificate Authority certificate (PEM + format) + + + )} + /> + + ( + + Client Certificate + + + + + Client certificate (PEM format) + + + )} + /> + + ( + + Client Key + + + + + Client private key (PEM format) + + + )} + /> +
+
+
+ )} + + ( + + API Version (Optional) + + + + + Specify Docker API version, or leave empty to use + the default + + + )} + /> + + )} +
0) ? ( -
+
{currentHostConfig?.quickActions && currentHostConfig.quickActions.length > 0 && (
@@ -600,20 +599,6 @@ export function Server({ )}
) : null} - - {currentHostConfig?.tunnelConnections && - currentHostConfig.tunnelConnections.length > 0 && ( -
- -
- )}
diff --git a/src/ui/desktop/apps/server/widgets/CpuWidget.tsx b/src/ui/desktop/apps/server-stats/widgets/CpuWidget.tsx similarity index 94% rename from src/ui/desktop/apps/server/widgets/CpuWidget.tsx rename to src/ui/desktop/apps/server-stats/widgets/CpuWidget.tsx index e05470f2..d241926c 100644 --- a/src/ui/desktop/apps/server/widgets/CpuWidget.tsx +++ b/src/ui/desktop/apps/server-stats/widgets/CpuWidget.tsx @@ -30,7 +30,7 @@ export function CpuWidget({ metrics, metricsHistory }: CpuWidgetProps) { }, [metricsHistory]); return ( -
+

diff --git a/src/ui/desktop/apps/server/widgets/DiskWidget.tsx b/src/ui/desktop/apps/server-stats/widgets/DiskWidget.tsx similarity index 94% rename from src/ui/desktop/apps/server/widgets/DiskWidget.tsx rename to src/ui/desktop/apps/server-stats/widgets/DiskWidget.tsx index 083b391d..25b469d9 100644 --- a/src/ui/desktop/apps/server/widgets/DiskWidget.tsx +++ b/src/ui/desktop/apps/server-stats/widgets/DiskWidget.tsx @@ -27,7 +27,7 @@ export function DiskWidget({ metrics }: DiskWidgetProps) { }, [metrics]); return ( -
+

diff --git a/src/ui/desktop/apps/server/widgets/LoginStatsWidget.tsx b/src/ui/desktop/apps/server-stats/widgets/LoginStatsWidget.tsx similarity index 96% rename from src/ui/desktop/apps/server/widgets/LoginStatsWidget.tsx rename to src/ui/desktop/apps/server-stats/widgets/LoginStatsWidget.tsx index f70e8727..432e724f 100644 --- a/src/ui/desktop/apps/server/widgets/LoginStatsWidget.tsx +++ b/src/ui/desktop/apps/server-stats/widgets/LoginStatsWidget.tsx @@ -35,7 +35,7 @@ export function LoginStatsWidget({ metrics }: LoginStatsWidgetProps) { const uniqueIPs = loginStats?.uniqueIPs || 0; return ( -
+

diff --git a/src/ui/desktop/apps/server/widgets/MemoryWidget.tsx b/src/ui/desktop/apps/server-stats/widgets/MemoryWidget.tsx similarity index 95% rename from src/ui/desktop/apps/server/widgets/MemoryWidget.tsx rename to src/ui/desktop/apps/server-stats/widgets/MemoryWidget.tsx index ba505a9d..3487fe88 100644 --- a/src/ui/desktop/apps/server/widgets/MemoryWidget.tsx +++ b/src/ui/desktop/apps/server-stats/widgets/MemoryWidget.tsx @@ -30,7 +30,7 @@ export function MemoryWidget({ metrics, metricsHistory }: MemoryWidgetProps) { }, [metricsHistory]); return ( -
+

diff --git a/src/ui/desktop/apps/server/widgets/NetworkWidget.tsx b/src/ui/desktop/apps/server-stats/widgets/NetworkWidget.tsx similarity index 93% rename from src/ui/desktop/apps/server/widgets/NetworkWidget.tsx rename to src/ui/desktop/apps/server-stats/widgets/NetworkWidget.tsx index 4a3e7379..597acd08 100644 --- a/src/ui/desktop/apps/server/widgets/NetworkWidget.tsx +++ b/src/ui/desktop/apps/server-stats/widgets/NetworkWidget.tsx @@ -24,7 +24,7 @@ export function NetworkWidget({ metrics }: NetworkWidgetProps) { const interfaces = network?.interfaces || []; return ( -
+

diff --git a/src/ui/desktop/apps/server/widgets/ProcessesWidget.tsx b/src/ui/desktop/apps/server-stats/widgets/ProcessesWidget.tsx similarity index 94% rename from src/ui/desktop/apps/server/widgets/ProcessesWidget.tsx rename to src/ui/desktop/apps/server-stats/widgets/ProcessesWidget.tsx index 2e51cec3..28c34448 100644 --- a/src/ui/desktop/apps/server/widgets/ProcessesWidget.tsx +++ b/src/ui/desktop/apps/server-stats/widgets/ProcessesWidget.tsx @@ -28,7 +28,7 @@ export function ProcessesWidget({ metrics }: ProcessesWidgetProps) { const topProcesses = processes?.top || []; return ( -
+

diff --git a/src/ui/desktop/apps/server/widgets/SystemWidget.tsx b/src/ui/desktop/apps/server-stats/widgets/SystemWidget.tsx similarity index 92% rename from src/ui/desktop/apps/server/widgets/SystemWidget.tsx rename to src/ui/desktop/apps/server-stats/widgets/SystemWidget.tsx index e9229d5b..2b38008d 100644 --- a/src/ui/desktop/apps/server/widgets/SystemWidget.tsx +++ b/src/ui/desktop/apps/server-stats/widgets/SystemWidget.tsx @@ -21,7 +21,7 @@ export function SystemWidget({ metrics }: SystemWidgetProps) { const system = metricsWithSystem?.system; return ( -
+

diff --git a/src/ui/desktop/apps/server/widgets/UptimeWidget.tsx b/src/ui/desktop/apps/server-stats/widgets/UptimeWidget.tsx similarity index 90% rename from src/ui/desktop/apps/server/widgets/UptimeWidget.tsx rename to src/ui/desktop/apps/server-stats/widgets/UptimeWidget.tsx index c8f02db7..04a72347 100644 --- a/src/ui/desktop/apps/server/widgets/UptimeWidget.tsx +++ b/src/ui/desktop/apps/server-stats/widgets/UptimeWidget.tsx @@ -20,7 +20,7 @@ export function UptimeWidget({ metrics }: UptimeWidgetProps) { const uptime = metricsWithUptime?.uptime; return ( -
+

diff --git a/src/ui/desktop/apps/server/widgets/index.ts b/src/ui/desktop/apps/server-stats/widgets/index.ts similarity index 100% rename from src/ui/desktop/apps/server/widgets/index.ts rename to src/ui/desktop/apps/server-stats/widgets/index.ts diff --git a/src/ui/desktop/apps/tunnel/TunnelManager.tsx b/src/ui/desktop/apps/tunnel/TunnelManager.tsx new file mode 100644 index 00000000..e280a594 --- /dev/null +++ b/src/ui/desktop/apps/tunnel/TunnelManager.tsx @@ -0,0 +1,143 @@ +import React from "react"; +import { useSidebar } from "@/components/ui/sidebar.tsx"; +import { Separator } from "@/components/ui/separator.tsx"; +import { Tunnel } from "@/ui/desktop/apps/tunnel/Tunnel.tsx"; +import { useTranslation } from "react-i18next"; + +interface HostConfig { + id: number; + name: string; + ip: string; + username: string; + folder?: string; + enableFileManager?: boolean; + tunnelConnections?: unknown[]; + [key: string]: unknown; +} + +interface TunnelManagerProps { + hostConfig?: HostConfig; + title?: string; + isVisible?: boolean; + isTopbarOpen?: boolean; + embedded?: boolean; +} + +export function TunnelManager({ + hostConfig, + title, + isVisible = true, + isTopbarOpen = true, + embedded = false, +}: TunnelManagerProps): React.ReactElement { + const { t } = useTranslation(); + const { state: sidebarState } = useSidebar(); + const [currentHostConfig, setCurrentHostConfig] = React.useState(hostConfig); + + React.useEffect(() => { + if (hostConfig?.id !== currentHostConfig?.id) { + setCurrentHostConfig(hostConfig); + } + }, [hostConfig?.id]); + + React.useEffect(() => { + const fetchLatestHostConfig = async () => { + if (hostConfig?.id) { + try { + const { getSSHHosts } = await import("@/ui/main-axios.ts"); + const hosts = await getSSHHosts(); + const updatedHost = hosts.find((h) => h.id === hostConfig.id); + if (updatedHost) { + setCurrentHostConfig(updatedHost); + } + } catch { + // Silently handle error + } + } + }; + + fetchLatestHostConfig(); + + const handleHostsChanged = async () => { + if (hostConfig?.id) { + try { + const { getSSHHosts } = await import("@/ui/main-axios.ts"); + const hosts = await getSSHHosts(); + const updatedHost = hosts.find((h) => h.id === hostConfig.id); + if (updatedHost) { + setCurrentHostConfig(updatedHost); + } + } catch { + // Silently handle error + } + } + }; + + window.addEventListener("ssh-hosts:changed", handleHostsChanged); + return () => + window.removeEventListener("ssh-hosts:changed", handleHostsChanged); + }, [hostConfig?.id]); + + const topMarginPx = isTopbarOpen ? 74 : 16; + const leftMarginPx = sidebarState === "collapsed" ? 16 : 8; + const bottomMarginPx = 8; + + const wrapperStyle: React.CSSProperties = embedded + ? { opacity: isVisible ? 1 : 0, height: "100%", width: "100%" } + : { + opacity: isVisible ? 1 : 0, + marginLeft: leftMarginPx, + marginRight: 17, + marginTop: topMarginPx, + marginBottom: bottomMarginPx, + height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`, + }; + + const containerClass = embedded + ? "h-full w-full text-white overflow-hidden bg-transparent" + : "bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden"; + + return ( +
+
+
+
+
+

+ {currentHostConfig?.folder} / {title} +

+
+
+
+ + +
+ {currentHostConfig?.tunnelConnections && + currentHostConfig.tunnelConnections.length > 0 ? ( +
+ +
+ ) : ( +
+
+

+ {t("tunnel.noTunnelsConfigured")} +

+

+ {t("tunnel.configureTunnelsInHostSettings")} +

+
+
+ )} +
+
+
+ ); +} diff --git a/src/ui/desktop/apps/tunnel/TunnelViewer.tsx b/src/ui/desktop/apps/tunnel/TunnelViewer.tsx index 10df33f7..28d22b73 100644 --- a/src/ui/desktop/apps/tunnel/TunnelViewer.tsx +++ b/src/ui/desktop/apps/tunnel/TunnelViewer.tsx @@ -43,11 +43,6 @@ export function TunnelViewer({ return (
-
-

- {t("tunnels.title")} -

-
{activeHost.tunnelConnections.map((t, idx) => ( diff --git a/src/ui/desktop/navigation/AppView.tsx b/src/ui/desktop/navigation/AppView.tsx index 51baf6e4..3d2f845c 100644 --- a/src/ui/desktop/navigation/AppView.tsx +++ b/src/ui/desktop/navigation/AppView.tsx @@ -1,7 +1,9 @@ import React, { useEffect, useRef, useState, useMemo } from "react"; import { Terminal } from "@/ui/desktop/apps/terminal/Terminal.tsx"; -import { Server as ServerView } from "@/ui/desktop/apps/server/Server.tsx"; +import { ServerStats as ServerView } from "@/ui/desktop/apps/server-stats/ServerStats.tsx"; import { FileManager } from "@/ui/desktop/apps/file-manager/FileManager.tsx"; +import { TunnelManager } from "@/ui/desktop/apps/tunnel/TunnelManager.tsx"; +import { DockerManager } from "@/ui/desktop/apps/docker/DockerManager.tsx"; import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; import { ResizablePanelGroup, @@ -58,7 +60,9 @@ export function AppView({ (tab: TabData) => tab.type === "terminal" || tab.type === "server" || - tab.type === "file_manager", + tab.type === "file_manager" || + tab.type === "tunnel" || + tab.type === "docker", ), [tabs], ); @@ -210,7 +214,10 @@ export function AppView({ const mainTab = terminalTabs.find((tab: TabData) => tab.id === currentTab); if (allSplitScreenTab.length === 0 && mainTab) { - const isFileManagerTab = mainTab.type === "file_manager"; + const isFileManagerTab = + mainTab.type === "file_manager" || + mainTab.type === "tunnel" || + mainTab.type === "docker"; const newStyle = { position: "absolute" as const, top: isFileManagerTab ? 0 : 4, @@ -257,9 +264,14 @@ export function AppView({ const isVisible = hasStyle || (allSplitScreenTab.length === 0 && t.id === currentTab); + const effectiveVisible = isVisible; + const previousStyle = previousStylesRef.current[t.id]; - const isFileManagerTab = t.type === "file_manager"; + const isFileManagerTab = + t.type === "file_manager" || + t.type === "tunnel" || + t.type === "docker"; const standardStyle = { position: "absolute" as const, top: isFileManagerTab ? 0 : 4, @@ -270,16 +282,24 @@ export function AppView({ const finalStyle: React.CSSProperties = hasStyle ? { ...styles[t.id], overflow: "hidden" } - : ({ - ...(previousStyle || standardStyle), - opacity: 0, - pointerEvents: "none", - zIndex: 0, - transition: "opacity 150ms ease-in-out", - overflow: "hidden", - } as React.CSSProperties); - - const effectiveVisible = isVisible; + : effectiveVisible + ? { + ...(previousStyle || standardStyle), + opacity: 1, + pointerEvents: "auto", + zIndex: 20, + display: "block", + transition: "opacity 150ms ease-in-out", + overflow: "hidden", + } + : ({ + ...(previousStyle || standardStyle), + opacity: 0, + pointerEvents: "none", + zIndex: 0, + transition: "opacity 150ms ease-in-out", + overflow: "hidden", + } as React.CSSProperties); const isTerminal = t.type === "terminal"; const terminalConfig = { @@ -317,6 +337,22 @@ export function AppView({ isTopbarOpen={isTopbarOpen} embedded /> + ) : t.type === "tunnel" ? ( + + ) : t.type === "docker" ? ( + ) : ( tab.id === currentTab); const isFileManager = currentTabData?.type === "file_manager"; + const isTunnel = currentTabData?.type === "tunnel"; + const isDocker = currentTabData?.type === "docker"; const isTerminal = currentTabData?.type === "terminal"; const isSplitScreen = allSplitScreenTab.length > 0; @@ -653,7 +691,7 @@ export function AppView({ const bottomMarginPx = 8; let containerBackground = "var(--color-dark-bg)"; - if (isFileManager && !isSplitScreen) { + if ((isFileManager || isTunnel || isDocker) && !isSplitScreen) { containerBackground = "var(--color-dark-bg-darkest)"; } else if (isTerminal) { containerBackground = terminalBackgroundColor; diff --git a/src/ui/desktop/navigation/TopNavbar.tsx b/src/ui/desktop/navigation/TopNavbar.tsx index 2cb11ef4..4838726c 100644 --- a/src/ui/desktop/navigation/TopNavbar.tsx +++ b/src/ui/desktop/navigation/TopNavbar.tsx @@ -369,10 +369,13 @@ export function TopNavbar({ const isTerminal = tab.type === "terminal"; const isServer = tab.type === "server"; const isFileManager = tab.type === "file_manager"; + const isTunnel = tab.type === "tunnel"; + const isDocker = tab.type === "docker"; const isSshManager = tab.type === "ssh_manager"; const isAdmin = tab.type === "admin"; const isUserProfile = tab.type === "user_profile"; - const isSplittable = isTerminal || isServer || isFileManager; + const isSplittable = + isTerminal || isServer || isFileManager || isTunnel || isDocker; const disableSplit = !isSplittable; const disableActivate = isSplit || @@ -484,6 +487,8 @@ export function TopNavbar({ isTerminal || isServer || isFileManager || + isTunnel || + isDocker || isSshManager || isAdmin || isUserProfile @@ -498,6 +503,8 @@ export function TopNavbar({ isTerminal || isServer || isFileManager || + isTunnel || + isDocker || isSshManager || isAdmin || isUserProfile diff --git a/src/ui/desktop/navigation/hosts/Host.tsx b/src/ui/desktop/navigation/hosts/Host.tsx index 18032159..f085f641 100644 --- a/src/ui/desktop/navigation/hosts/Host.tsx +++ b/src/ui/desktop/navigation/hosts/Host.tsx @@ -8,6 +8,8 @@ import { Server, FolderOpen, Pencil, + ArrowDownUp, + Container, } from "lucide-react"; import { DropdownMenu, @@ -63,6 +65,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement { }, [host.statsConfig]); const shouldShowStatus = statsConfig.statusCheckEnabled !== false; + const shouldShowMetrics = statsConfig.metricsEnabled !== false; useEffect(() => { if (!shouldShowStatus) { @@ -151,24 +154,50 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement { side="right" className="w-56 bg-dark-bg border-dark-border text-white" > - - addTab({ type: "server", title, hostConfig: host }) - } - className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300" - > - - Open Server Details - - - addTab({ type: "file_manager", title, hostConfig: host }) - } - className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300" - > - - Open File Manager - + {shouldShowMetrics && ( + + addTab({ type: "server", title, hostConfig: host }) + } + className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300" + > + + Open Server Stats + + )} + {host.enableFileManager && ( + + addTab({ type: "file_manager", title, hostConfig: host }) + } + className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300" + > + + Open File Manager + + )} + {host.enableTunnel && ( + + addTab({ type: "tunnel", title, hostConfig: host }) + } + className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300" + > + + Open Tunnels + + )} + {host.enableDocker && ( + + addTab({ type: "docker", title, hostConfig: host }) + } + className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300" + > + + Open Docker + + )} addTab({ diff --git a/src/ui/desktop/navigation/tabs/Tab.tsx b/src/ui/desktop/navigation/tabs/Tab.tsx index 18dc75e0..0d9f2c1e 100644 --- a/src/ui/desktop/navigation/tabs/Tab.tsx +++ b/src/ui/desktop/navigation/tabs/Tab.tsx @@ -10,6 +10,8 @@ import { Server as ServerIcon, Folder as FolderIcon, User as UserIcon, + ArrowDownUp as TunnelIcon, + Container as DockerIcon, } from "lucide-react"; interface TabProps { @@ -119,10 +121,14 @@ export function Tab({ tabType === "terminal" || tabType === "server" || tabType === "file_manager" || + tabType === "tunnel" || + tabType === "docker" || tabType === "user_profile" ) { const isServer = tabType === "server"; const isFileManager = tabType === "file_manager"; + const isTunnel = tabType === "tunnel"; + const isDocker = tabType === "docker"; const isUserProfile = tabType === "user_profile"; const displayTitle = @@ -131,9 +137,13 @@ export function Tab({ ? t("nav.serverStats") : isFileManager ? t("nav.fileManager") - : isUserProfile - ? t("nav.userProfile") - : t("nav.terminal")); + : isTunnel + ? t("nav.tunnels") + : isDocker + ? t("nav.docker") + : isUserProfile + ? t("nav.userProfile") + : t("nav.terminal")); const { base, suffix } = splitTitle(displayTitle); @@ -151,6 +161,10 @@ export function Tab({ ) : isFileManager ? ( + ) : isTunnel ? ( + + ) : isDocker ? ( + ) : isUserProfile ? ( ) : ( diff --git a/src/ui/desktop/navigation/tabs/TabContext.tsx b/src/ui/desktop/navigation/tabs/TabContext.tsx index 7bcd6fe9..61347d6b 100644 --- a/src/ui/desktop/navigation/tabs/TabContext.tsx +++ b/src/ui/desktop/navigation/tabs/TabContext.tsx @@ -76,7 +76,11 @@ export function TabProvider({ children }: TabProviderProps) { ? t("nav.serverStats") : tabType === "file_manager" ? t("nav.fileManager") - : t("nav.terminal"); + : tabType === "tunnel" + ? t("nav.tunnels") + : tabType === "docker" + ? t("nav.docker") + : t("nav.terminal"); const baseTitle = (desiredTitle || defaultTitle).trim(); const match = baseTitle.match(/^(.*) \((\d+)\)$/); const root = match ? match[1] : baseTitle; @@ -137,7 +141,9 @@ export function TabProvider({ children }: TabProviderProps) { const needsUniqueTitle = tabData.type === "terminal" || tabData.type === "server" || - tabData.type === "file_manager"; + tabData.type === "file_manager" || + tabData.type === "tunnel" || + tabData.type === "docker"; const effectiveTitle = needsUniqueTitle ? computeUniqueTitle(tabData.type, tabData.title) : tabData.title || ""; diff --git a/src/ui/desktop/navigation/tabs/TabDropdown.tsx b/src/ui/desktop/navigation/tabs/TabDropdown.tsx index 578a550b..fdc2a711 100644 --- a/src/ui/desktop/navigation/tabs/TabDropdown.tsx +++ b/src/ui/desktop/navigation/tabs/TabDropdown.tsx @@ -12,6 +12,8 @@ import { Terminal as TerminalIcon, Server as ServerIcon, Folder as FolderIcon, + ArrowDownUp as TunnelIcon, + Container as DockerIcon, Shield as AdminIcon, Network as SshManagerIcon, User as UserIcon, @@ -33,6 +35,10 @@ export function TabDropdown(): React.ReactElement { return ; case "file_manager": return ; + case "tunnel": + return ; + case "docker": + return ; case "user_profile": return ; case "ssh_manager": @@ -52,6 +58,10 @@ export function TabDropdown(): React.ReactElement { return tab.title || t("nav.serverStats"); case "file_manager": return tab.title || t("nav.fileManager"); + case "tunnel": + return tab.title || t("nav.tunnels"); + case "docker": + return tab.title || t("nav.docker"); case "user_profile": return tab.title || t("nav.userProfile"); case "ssh_manager": diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index 0bd7ee5b..f1c4a4ab 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -856,6 +856,7 @@ export async function createSSHHost(hostData: SSHHostData): Promise { enableTerminal: Boolean(hostData.enableTerminal), enableTunnel: Boolean(hostData.enableTunnel), enableFileManager: Boolean(hostData.enableFileManager), + enableDocker: Boolean(hostData.enableDocker), defaultPath: hostData.defaultPath || "/", tunnelConnections: hostData.tunnelConnections || [], jumpHosts: hostData.jumpHosts || [], @@ -865,6 +866,11 @@ export async function createSSHHost(hostData: SSHHostData): Promise { ? hostData.statsConfig : JSON.stringify(hostData.statsConfig) : null, + dockerConfig: hostData.dockerConfig + ? typeof hostData.dockerConfig === "string" + ? hostData.dockerConfig + : JSON.stringify(hostData.dockerConfig) + : null, terminalConfig: hostData.terminalConfig || null, forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive), }; @@ -922,6 +928,7 @@ export async function updateSSHHost( enableTerminal: Boolean(hostData.enableTerminal), enableTunnel: Boolean(hostData.enableTunnel), enableFileManager: Boolean(hostData.enableFileManager), + enableDocker: Boolean(hostData.enableDocker), defaultPath: hostData.defaultPath || "/", tunnelConnections: hostData.tunnelConnections || [], jumpHosts: hostData.jumpHosts || [], @@ -931,6 +938,11 @@ export async function updateSSHHost( ? hostData.statsConfig : JSON.stringify(hostData.statsConfig) : null, + dockerConfig: hostData.dockerConfig + ? typeof hostData.dockerConfig === "string" + ? hostData.dockerConfig + : JSON.stringify(hostData.dockerConfig) + : null, terminalConfig: hostData.terminalConfig || null, forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive), };