feat: Seperate server stats and tunnel management (improved both UI's) then started initial docker implementation
This commit is contained in:
@@ -86,8 +86,12 @@ export const sshData = sqliteTable("ssh_data", {
|
|||||||
enableFileManager: integer("enable_file_manager", { mode: "boolean" })
|
enableFileManager: integer("enable_file_manager", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(true),
|
.default(true),
|
||||||
|
enableDocker: integer("enable_docker", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
defaultPath: text("default_path"),
|
defaultPath: text("default_path"),
|
||||||
statsConfig: text("stats_config"),
|
statsConfig: text("stats_config"),
|
||||||
|
dockerConfig: text("docker_config"),
|
||||||
terminalConfig: text("terminal_config"),
|
terminalConfig: text("terminal_config"),
|
||||||
quickActions: text("quick_actions"),
|
quickActions: text("quick_actions"),
|
||||||
createdAt: text("created_at")
|
createdAt: text("created_at")
|
||||||
|
|||||||
@@ -235,11 +235,13 @@ router.post(
|
|||||||
enableTerminal,
|
enableTerminal,
|
||||||
enableTunnel,
|
enableTunnel,
|
||||||
enableFileManager,
|
enableFileManager,
|
||||||
|
enableDocker,
|
||||||
defaultPath,
|
defaultPath,
|
||||||
tunnelConnections,
|
tunnelConnections,
|
||||||
jumpHosts,
|
jumpHosts,
|
||||||
quickActions,
|
quickActions,
|
||||||
statsConfig,
|
statsConfig,
|
||||||
|
dockerConfig,
|
||||||
terminalConfig,
|
terminalConfig,
|
||||||
forceKeyboardInteractive,
|
forceKeyboardInteractive,
|
||||||
} = hostData;
|
} = hostData;
|
||||||
@@ -280,8 +282,10 @@ router.post(
|
|||||||
? JSON.stringify(quickActions)
|
? JSON.stringify(quickActions)
|
||||||
: null,
|
: null,
|
||||||
enableFileManager: enableFileManager ? 1 : 0,
|
enableFileManager: enableFileManager ? 1 : 0,
|
||||||
|
enableDocker: enableDocker ? 1 : 0,
|
||||||
defaultPath: defaultPath || null,
|
defaultPath: defaultPath || null,
|
||||||
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
|
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
|
||||||
|
dockerConfig: dockerConfig ? JSON.stringify(dockerConfig) : null,
|
||||||
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
|
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
|
||||||
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
||||||
};
|
};
|
||||||
@@ -341,9 +345,13 @@ router.post(
|
|||||||
? JSON.parse(createdHost.jumpHosts as string)
|
? JSON.parse(createdHost.jumpHosts as string)
|
||||||
: [],
|
: [],
|
||||||
enableFileManager: !!createdHost.enableFileManager,
|
enableFileManager: !!createdHost.enableFileManager,
|
||||||
|
enableDocker: !!createdHost.enableDocker,
|
||||||
statsConfig: createdHost.statsConfig
|
statsConfig: createdHost.statsConfig
|
||||||
? JSON.parse(createdHost.statsConfig as string)
|
? JSON.parse(createdHost.statsConfig as string)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
dockerConfig: createdHost.dockerConfig
|
||||||
|
? JSON.parse(createdHost.dockerConfig as string)
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost;
|
const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost;
|
||||||
@@ -457,11 +465,13 @@ router.put(
|
|||||||
enableTerminal,
|
enableTerminal,
|
||||||
enableTunnel,
|
enableTunnel,
|
||||||
enableFileManager,
|
enableFileManager,
|
||||||
|
enableDocker,
|
||||||
defaultPath,
|
defaultPath,
|
||||||
tunnelConnections,
|
tunnelConnections,
|
||||||
jumpHosts,
|
jumpHosts,
|
||||||
quickActions,
|
quickActions,
|
||||||
statsConfig,
|
statsConfig,
|
||||||
|
dockerConfig,
|
||||||
terminalConfig,
|
terminalConfig,
|
||||||
forceKeyboardInteractive,
|
forceKeyboardInteractive,
|
||||||
} = hostData;
|
} = hostData;
|
||||||
@@ -503,8 +513,10 @@ router.put(
|
|||||||
? JSON.stringify(quickActions)
|
? JSON.stringify(quickActions)
|
||||||
: null,
|
: null,
|
||||||
enableFileManager: enableFileManager ? 1 : 0,
|
enableFileManager: enableFileManager ? 1 : 0,
|
||||||
|
enableDocker: enableDocker ? 1 : 0,
|
||||||
defaultPath: defaultPath || null,
|
defaultPath: defaultPath || null,
|
||||||
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
|
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
|
||||||
|
dockerConfig: dockerConfig ? JSON.stringify(dockerConfig) : null,
|
||||||
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
|
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
|
||||||
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
||||||
};
|
};
|
||||||
@@ -582,9 +594,13 @@ router.put(
|
|||||||
? JSON.parse(updatedHost.jumpHosts as string)
|
? JSON.parse(updatedHost.jumpHosts as string)
|
||||||
: [],
|
: [],
|
||||||
enableFileManager: !!updatedHost.enableFileManager,
|
enableFileManager: !!updatedHost.enableFileManager,
|
||||||
|
enableDocker: !!updatedHost.enableDocker,
|
||||||
statsConfig: updatedHost.statsConfig
|
statsConfig: updatedHost.statsConfig
|
||||||
? JSON.parse(updatedHost.statsConfig as string)
|
? JSON.parse(updatedHost.statsConfig as string)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
dockerConfig: updatedHost.dockerConfig
|
||||||
|
? JSON.parse(updatedHost.dockerConfig as string)
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost;
|
const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost;
|
||||||
@@ -683,9 +699,13 @@ router.get(
|
|||||||
? JSON.parse(row.quickActions as string)
|
? JSON.parse(row.quickActions as string)
|
||||||
: [],
|
: [],
|
||||||
enableFileManager: !!row.enableFileManager,
|
enableFileManager: !!row.enableFileManager,
|
||||||
|
enableDocker: !!row.enableDocker,
|
||||||
statsConfig: row.statsConfig
|
statsConfig: row.statsConfig
|
||||||
? JSON.parse(row.statsConfig as string)
|
? JSON.parse(row.statsConfig as string)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
dockerConfig: row.dockerConfig
|
||||||
|
? JSON.parse(row.dockerConfig as string)
|
||||||
|
: undefined,
|
||||||
terminalConfig: row.terminalConfig
|
terminalConfig: row.terminalConfig
|
||||||
? JSON.parse(row.terminalConfig as string)
|
? JSON.parse(row.terminalConfig as string)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|||||||
@@ -14,6 +14,18 @@ export interface QuickAction {
|
|||||||
snippetId: number;
|
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 {
|
export interface SSHHost {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -40,11 +52,13 @@ export interface SSHHost {
|
|||||||
enableTerminal: boolean;
|
enableTerminal: boolean;
|
||||||
enableTunnel: boolean;
|
enableTunnel: boolean;
|
||||||
enableFileManager: boolean;
|
enableFileManager: boolean;
|
||||||
|
enableDocker: boolean;
|
||||||
defaultPath: string;
|
defaultPath: string;
|
||||||
tunnelConnections: TunnelConnection[];
|
tunnelConnections: TunnelConnection[];
|
||||||
jumpHosts?: JumpHost[];
|
jumpHosts?: JumpHost[];
|
||||||
quickActions?: QuickAction[];
|
quickActions?: QuickAction[];
|
||||||
statsConfig?: string;
|
statsConfig?: string;
|
||||||
|
dockerConfig?: string;
|
||||||
terminalConfig?: TerminalConfig;
|
terminalConfig?: TerminalConfig;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -77,12 +91,14 @@ export interface SSHHostData {
|
|||||||
enableTerminal?: boolean;
|
enableTerminal?: boolean;
|
||||||
enableTunnel?: boolean;
|
enableTunnel?: boolean;
|
||||||
enableFileManager?: boolean;
|
enableFileManager?: boolean;
|
||||||
|
enableDocker?: boolean;
|
||||||
defaultPath?: string;
|
defaultPath?: string;
|
||||||
forceKeyboardInteractive?: boolean;
|
forceKeyboardInteractive?: boolean;
|
||||||
tunnelConnections?: TunnelConnection[];
|
tunnelConnections?: TunnelConnection[];
|
||||||
jumpHosts?: JumpHostData[];
|
jumpHosts?: JumpHostData[];
|
||||||
quickActions?: QuickActionData[];
|
quickActions?: QuickActionData[];
|
||||||
statsConfig?: string | Record<string, unknown>;
|
statsConfig?: string | Record<string, unknown>;
|
||||||
|
dockerConfig?: DockerConfig | string;
|
||||||
terminalConfig?: TerminalConfig;
|
terminalConfig?: TerminalConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +361,8 @@ export interface TabContextTab {
|
|||||||
| "server"
|
| "server"
|
||||||
| "admin"
|
| "admin"
|
||||||
| "file_manager"
|
| "file_manager"
|
||||||
| "user_profile";
|
| "user_profile"
|
||||||
|
| "docker";
|
||||||
title: string;
|
title: string;
|
||||||
hostConfig?: SSHHost;
|
hostConfig?: SSHHost;
|
||||||
terminalRef?: any;
|
terminalRef?: any;
|
||||||
|
|||||||
@@ -155,7 +155,9 @@ function AppContent() {
|
|||||||
const showTerminalView =
|
const showTerminalView =
|
||||||
currentTabData?.type === "terminal" ||
|
currentTabData?.type === "terminal" ||
|
||||||
currentTabData?.type === "server" ||
|
currentTabData?.type === "server" ||
|
||||||
currentTabData?.type === "file_manager";
|
currentTabData?.type === "file_manager" ||
|
||||||
|
currentTabData?.type === "tunnel" ||
|
||||||
|
currentTabData?.type === "docker";
|
||||||
const showHome = currentTabData?.type === "home";
|
const showHome = currentTabData?.type === "home";
|
||||||
const showSshManager = currentTabData?.type === "ssh_manager";
|
const showSshManager = currentTabData?.type === "ssh_manager";
|
||||||
const showAdmin = currentTabData?.type === "admin";
|
const showAdmin = currentTabData?.type === "admin";
|
||||||
|
|||||||
126
src/ui/desktop/apps/docker/DockerManager.tsx
Normal file
126
src/ui/desktop/apps/docker/DockerManager.tsx
Normal file
@@ -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 (
|
||||||
|
<div style={wrapperStyle} className={containerClass}>
|
||||||
|
<div className="h-full w-full flex flex-col">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between px-4 pt-3 pb-3 gap-3">
|
||||||
|
<div className="flex items-center gap-4 min-w-0">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h1 className="font-bold text-lg truncate">
|
||||||
|
{currentHostConfig?.folder} / {title}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator className="p-0.25 w-full" />
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden min-h-0 p-1">
|
||||||
|
{/* Empty body as requested */}
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-gray-400 text-lg">
|
||||||
|
Docker management UI will be here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -566,6 +566,20 @@ export function HostManagerEditor({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.default([]),
|
.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) => {
|
.superRefine((data, ctx) => {
|
||||||
if (data.authType === "none") {
|
if (data.authType === "none") {
|
||||||
@@ -658,6 +672,18 @@ export function HostManagerEditor({
|
|||||||
statsConfig: DEFAULT_STATS_CONFIG,
|
statsConfig: DEFAULT_STATS_CONFIG,
|
||||||
terminalConfig: DEFAULT_TERMINAL_CONFIG,
|
terminalConfig: DEFAULT_TERMINAL_CONFIG,
|
||||||
forceKeyboardInteractive: false,
|
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),
|
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") {
|
if (defaultAuthType === "password") {
|
||||||
@@ -804,6 +842,18 @@ export function HostManagerEditor({
|
|||||||
statsConfig: DEFAULT_STATS_CONFIG,
|
statsConfig: DEFAULT_STATS_CONFIG,
|
||||||
terminalConfig: DEFAULT_TERMINAL_CONFIG,
|
terminalConfig: DEFAULT_TERMINAL_CONFIG,
|
||||||
forceKeyboardInteractive: false,
|
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);
|
form.reset(defaultFormData);
|
||||||
@@ -861,6 +911,8 @@ export function HostManagerEditor({
|
|||||||
authType: data.authType,
|
authType: data.authType,
|
||||||
overrideCredentialUsername: Boolean(data.overrideCredentialUsername),
|
overrideCredentialUsername: Boolean(data.overrideCredentialUsername),
|
||||||
enableTerminal: Boolean(data.enableTerminal),
|
enableTerminal: Boolean(data.enableTerminal),
|
||||||
|
enableDocker: Boolean(data.enableDocker),
|
||||||
|
dockerConfig: data.dockerConfig || null,
|
||||||
enableTunnel: Boolean(data.enableTunnel),
|
enableTunnel: Boolean(data.enableTunnel),
|
||||||
enableFileManager: Boolean(data.enableFileManager),
|
enableFileManager: Boolean(data.enableFileManager),
|
||||||
defaultPath: data.defaultPath || "/",
|
defaultPath: data.defaultPath || "/",
|
||||||
@@ -948,9 +1000,8 @@ export function HostManagerEditor({
|
|||||||
window.dispatchEvent(new CustomEvent("ssh-hosts:changed"));
|
window.dispatchEvent(new CustomEvent("ssh-hosts:changed"));
|
||||||
|
|
||||||
if (savedHost?.id) {
|
if (savedHost?.id) {
|
||||||
const { notifyHostCreatedOrUpdated } = await import(
|
const { notifyHostCreatedOrUpdated } =
|
||||||
"@/ui/main-axios.ts"
|
await import("@/ui/main-axios.ts");
|
||||||
);
|
|
||||||
notifyHostCreatedOrUpdated(savedHost.id);
|
notifyHostCreatedOrUpdated(savedHost.id);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -983,6 +1034,8 @@ export function HostManagerEditor({
|
|||||||
setActiveTab("general");
|
setActiveTab("general");
|
||||||
} else if (errors.enableTerminal || errors.terminalConfig) {
|
} else if (errors.enableTerminal || errors.terminalConfig) {
|
||||||
setActiveTab("terminal");
|
setActiveTab("terminal");
|
||||||
|
} else if (errors.enableDocker || errors.dockerConfig) {
|
||||||
|
setActiveTab("docker");
|
||||||
} else if (errors.enableTunnel || errors.tunnelConnections) {
|
} else if (errors.enableTunnel || errors.tunnelConnections) {
|
||||||
setActiveTab("tunnel");
|
setActiveTab("tunnel");
|
||||||
} else if (errors.enableFileManager || errors.defaultPath) {
|
} else if (errors.enableFileManager || errors.defaultPath) {
|
||||||
@@ -1175,6 +1228,7 @@ export function HostManagerEditor({
|
|||||||
<TabsTrigger value="terminal">
|
<TabsTrigger value="terminal">
|
||||||
{t("hosts.terminal")}
|
{t("hosts.terminal")}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="docker">Docker</TabsTrigger>
|
||||||
<TabsTrigger value="tunnel">{t("hosts.tunnel")}</TabsTrigger>
|
<TabsTrigger value="tunnel">{t("hosts.tunnel")}</TabsTrigger>
|
||||||
<TabsTrigger value="file_manager">
|
<TabsTrigger value="file_manager">
|
||||||
{t("hosts.fileManager")}
|
{t("hosts.fileManager")}
|
||||||
@@ -2545,6 +2599,307 @@ export function HostManagerEditor({
|
|||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="docker" className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enableDocker"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Enable Docker</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Enable Docker integration for this host
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.watch("enableDocker") && (
|
||||||
|
<>
|
||||||
|
<Alert className="mt-4">
|
||||||
|
<AlertDescription>
|
||||||
|
<strong>Docker Configuration</strong>
|
||||||
|
<div className="mt-2">
|
||||||
|
Configure connection to Docker daemon on this host.
|
||||||
|
You can connect via Unix socket, TCP, or secure TLS
|
||||||
|
connection.
|
||||||
|
</div>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dockerConfig.connectionType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Connection Type</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select connection type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="socket">
|
||||||
|
Unix Socket
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="tcp">TCP</SelectItem>
|
||||||
|
<SelectItem value="tls">
|
||||||
|
TCP with TLS
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
Choose how to connect to the Docker daemon
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.watch("dockerConfig.connectionType") ===
|
||||||
|
"socket" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dockerConfig.socketPath"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Socket Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="/var/run/docker.sock"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Path to the Docker Unix socket (default:
|
||||||
|
/var/run/docker.sock)
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(form.watch("dockerConfig.connectionType") === "tcp" ||
|
||||||
|
form.watch("dockerConfig.connectionType") ===
|
||||||
|
"tls") && (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-12 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dockerConfig.host"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="col-span-8">
|
||||||
|
<FormLabel>Docker Host</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="localhost or IP address"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dockerConfig.port"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="col-span-4">
|
||||||
|
<FormLabel>Port</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="2375 or 2376"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{form.watch("dockerConfig.connectionType") === "tls" && (
|
||||||
|
<Accordion type="multiple" className="w-full mt-4">
|
||||||
|
<AccordionItem value="tls-config">
|
||||||
|
<AccordionTrigger>
|
||||||
|
TLS Configuration
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="space-y-4 pt-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dockerConfig.tlsVerify"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Verify TLS</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Verify the Docker daemon's certificate
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dockerConfig.tlsCaCert"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>CA Certificate</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<CodeMirror
|
||||||
|
value={field.value || ""}
|
||||||
|
onChange={field.onChange}
|
||||||
|
placeholder="-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----"
|
||||||
|
theme={oneDark}
|
||||||
|
className="border border-input rounded-md"
|
||||||
|
minHeight="120px"
|
||||||
|
basicSetup={{
|
||||||
|
lineNumbers: true,
|
||||||
|
foldGutter: false,
|
||||||
|
dropCursor: false,
|
||||||
|
allowMultipleSelections: false,
|
||||||
|
highlightSelectionMatches: false,
|
||||||
|
searchKeymap: false,
|
||||||
|
scrollPastEnd: false,
|
||||||
|
}}
|
||||||
|
extensions={[
|
||||||
|
EditorView.theme({
|
||||||
|
".cm-scroller": {
|
||||||
|
overflow: "auto",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Certificate Authority certificate (PEM
|
||||||
|
format)
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dockerConfig.tlsCert"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Client Certificate</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<CodeMirror
|
||||||
|
value={field.value || ""}
|
||||||
|
onChange={field.onChange}
|
||||||
|
placeholder="-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----"
|
||||||
|
theme={oneDark}
|
||||||
|
className="border border-input rounded-md"
|
||||||
|
minHeight="120px"
|
||||||
|
basicSetup={{
|
||||||
|
lineNumbers: true,
|
||||||
|
foldGutter: false,
|
||||||
|
dropCursor: false,
|
||||||
|
allowMultipleSelections: false,
|
||||||
|
highlightSelectionMatches: false,
|
||||||
|
searchKeymap: false,
|
||||||
|
scrollPastEnd: false,
|
||||||
|
}}
|
||||||
|
extensions={[
|
||||||
|
EditorView.theme({
|
||||||
|
".cm-scroller": {
|
||||||
|
overflow: "auto",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Client certificate (PEM format)
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dockerConfig.tlsKey"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Client Key</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<CodeMirror
|
||||||
|
value={field.value || ""}
|
||||||
|
onChange={field.onChange}
|
||||||
|
placeholder="-----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY-----"
|
||||||
|
theme={oneDark}
|
||||||
|
className="border border-input rounded-md"
|
||||||
|
minHeight="120px"
|
||||||
|
basicSetup={{
|
||||||
|
lineNumbers: true,
|
||||||
|
foldGutter: false,
|
||||||
|
dropCursor: false,
|
||||||
|
allowMultipleSelections: false,
|
||||||
|
highlightSelectionMatches: false,
|
||||||
|
searchKeymap: false,
|
||||||
|
scrollPastEnd: false,
|
||||||
|
}}
|
||||||
|
extensions={[
|
||||||
|
EditorView.theme({
|
||||||
|
".cm-scroller": {
|
||||||
|
overflow: "auto",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Client private key (PEM format)
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dockerConfig.apiVersion"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>API Version (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="1.41 (leave empty for auto)"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Specify Docker API version, or leave empty to use
|
||||||
|
the default
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
<TabsContent value="tunnel">
|
<TabsContent value="tunnel">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useSidebar } from "@/components/ui/sidebar.tsx";
|
|||||||
import { Status, StatusIndicator } from "@/components/ui/shadcn-io/status";
|
import { Status, StatusIndicator } from "@/components/ui/shadcn-io/status";
|
||||||
import { Separator } from "@/components/ui/separator.tsx";
|
import { Separator } from "@/components/ui/separator.tsx";
|
||||||
import { Button } from "@/components/ui/button.tsx";
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
import { Tunnel } from "@/ui/desktop/apps/tunnel/Tunnel.tsx";
|
|
||||||
import {
|
import {
|
||||||
getServerStatusById,
|
getServerStatusById,
|
||||||
getServerMetricsById,
|
getServerMetricsById,
|
||||||
@@ -64,7 +63,7 @@ interface ServerProps {
|
|||||||
embedded?: boolean;
|
embedded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Server({
|
export function ServerStats({
|
||||||
hostConfig,
|
hostConfig,
|
||||||
title,
|
title,
|
||||||
isVisible = true,
|
isVisible = true,
|
||||||
@@ -462,7 +461,7 @@ export function Server({
|
|||||||
{(metricsEnabled && showStatsUI) ||
|
{(metricsEnabled && showStatsUI) ||
|
||||||
(currentHostConfig?.quickActions &&
|
(currentHostConfig?.quickActions &&
|
||||||
currentHostConfig.quickActions.length > 0) ? (
|
currentHostConfig.quickActions.length > 0) ? (
|
||||||
<div className="rounded-lg border-2 border-dark-border m-3 bg-dark-bg-darker p-4 overflow-y-auto relative flex-1 flex flex-col">
|
<div className="rounded-lg border-2 border-dark-border m-3 p-4 overflow-y-auto relative flex-1 flex flex-col">
|
||||||
{currentHostConfig?.quickActions &&
|
{currentHostConfig?.quickActions &&
|
||||||
currentHostConfig.quickActions.length > 0 && (
|
currentHostConfig.quickActions.length > 0 && (
|
||||||
<div className={metricsEnabled && showStatsUI ? "mb-4" : ""}>
|
<div className={metricsEnabled && showStatsUI ? "mb-4" : ""}>
|
||||||
@@ -600,20 +599,6 @@ export function Server({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{currentHostConfig?.tunnelConnections &&
|
|
||||||
currentHostConfig.tunnelConnections.length > 0 && (
|
|
||||||
<div className="rounded-lg border-2 border-dark-border m-3 bg-dark-bg-darker h-[360px] overflow-hidden flex flex-col min-h-0">
|
|
||||||
<Tunnel
|
|
||||||
filterHostKey={
|
|
||||||
currentHostConfig?.name &&
|
|
||||||
currentHostConfig.name.trim() !== ""
|
|
||||||
? currentHostConfig.name
|
|
||||||
: `${currentHostConfig?.username}@${currentHostConfig?.ip}`
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,7 +30,7 @@ export function CpuWidget({ metrics, metricsHistory }: CpuWidgetProps) {
|
|||||||
}, [metricsHistory]);
|
}, [metricsHistory]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
<div className="h-full w-full p-4 rounded-lg bg-dark-bg-darker border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
||||||
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
||||||
<Cpu className="h-5 w-5 text-blue-400" />
|
<Cpu className="h-5 w-5 text-blue-400" />
|
||||||
<h3 className="font-semibold text-lg text-white">
|
<h3 className="font-semibold text-lg text-white">
|
||||||
@@ -27,7 +27,7 @@ export function DiskWidget({ metrics }: DiskWidgetProps) {
|
|||||||
}, [metrics]);
|
}, [metrics]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
<div className="h-full w-full p-4 rounded-lg bg-dark-bg-darker border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
||||||
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
||||||
<HardDrive className="h-5 w-5 text-orange-400" />
|
<HardDrive className="h-5 w-5 text-orange-400" />
|
||||||
<h3 className="font-semibold text-lg text-white">
|
<h3 className="font-semibold text-lg text-white">
|
||||||
@@ -35,7 +35,7 @@ export function LoginStatsWidget({ metrics }: LoginStatsWidgetProps) {
|
|||||||
const uniqueIPs = loginStats?.uniqueIPs || 0;
|
const uniqueIPs = loginStats?.uniqueIPs || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
<div className="h-full w-full p-4 rounded-lg bg-dark-bg-darker border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
||||||
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
||||||
<UserCheck className="h-5 w-5 text-green-400" />
|
<UserCheck className="h-5 w-5 text-green-400" />
|
||||||
<h3 className="font-semibold text-lg text-white">
|
<h3 className="font-semibold text-lg text-white">
|
||||||
@@ -30,7 +30,7 @@ export function MemoryWidget({ metrics, metricsHistory }: MemoryWidgetProps) {
|
|||||||
}, [metricsHistory]);
|
}, [metricsHistory]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
<div className="h-full w-full p-4 rounded-lg bg-dark-bg-darker border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
||||||
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
||||||
<MemoryStick className="h-5 w-5 text-green-400" />
|
<MemoryStick className="h-5 w-5 text-green-400" />
|
||||||
<h3 className="font-semibold text-lg text-white">
|
<h3 className="font-semibold text-lg text-white">
|
||||||
@@ -24,7 +24,7 @@ export function NetworkWidget({ metrics }: NetworkWidgetProps) {
|
|||||||
const interfaces = network?.interfaces || [];
|
const interfaces = network?.interfaces || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
<div className="h-full w-full p-4 rounded-lg bg-dark-bg-darker border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
||||||
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
||||||
<Network className="h-5 w-5 text-indigo-400" />
|
<Network className="h-5 w-5 text-indigo-400" />
|
||||||
<h3 className="font-semibold text-lg text-white">
|
<h3 className="font-semibold text-lg text-white">
|
||||||
@@ -28,7 +28,7 @@ export function ProcessesWidget({ metrics }: ProcessesWidgetProps) {
|
|||||||
const topProcesses = processes?.top || [];
|
const topProcesses = processes?.top || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
<div className="h-full w-full p-4 rounded-lg bg-dark-bg-darker border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
||||||
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
||||||
<List className="h-5 w-5 text-yellow-400" />
|
<List className="h-5 w-5 text-yellow-400" />
|
||||||
<h3 className="font-semibold text-lg text-white">
|
<h3 className="font-semibold text-lg text-white">
|
||||||
@@ -21,7 +21,7 @@ export function SystemWidget({ metrics }: SystemWidgetProps) {
|
|||||||
const system = metricsWithSystem?.system;
|
const system = metricsWithSystem?.system;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
<div className="h-full w-full p-4 rounded-lg bg-dark-bg-darker border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
||||||
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
||||||
<Server className="h-5 w-5 text-purple-400" />
|
<Server className="h-5 w-5 text-purple-400" />
|
||||||
<h3 className="font-semibold text-lg text-white">
|
<h3 className="font-semibold text-lg text-white">
|
||||||
@@ -20,7 +20,7 @@ export function UptimeWidget({ metrics }: UptimeWidgetProps) {
|
|||||||
const uptime = metricsWithUptime?.uptime;
|
const uptime = metricsWithUptime?.uptime;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full p-4 rounded-lg bg-dark-bg/50 border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
<div className="h-full w-full p-4 rounded-lg bg-dark-bg-darker border border-dark-border/50 hover:bg-dark-bg/70 transition-colors duration-200 flex flex-col overflow-hidden">
|
||||||
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
<div className="flex items-center gap-2 flex-shrink-0 mb-3">
|
||||||
<Clock className="h-5 w-5 text-cyan-400" />
|
<Clock className="h-5 w-5 text-cyan-400" />
|
||||||
<h3 className="font-semibold text-lg text-white">
|
<h3 className="font-semibold text-lg text-white">
|
||||||
143
src/ui/desktop/apps/tunnel/TunnelManager.tsx
Normal file
143
src/ui/desktop/apps/tunnel/TunnelManager.tsx
Normal file
@@ -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 (
|
||||||
|
<div style={wrapperStyle} className={containerClass}>
|
||||||
|
<div className="h-full w-full flex flex-col">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between px-4 pt-3 pb-3 gap-3">
|
||||||
|
<div className="flex items-center gap-4 min-w-0">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h1 className="font-bold text-lg truncate">
|
||||||
|
{currentHostConfig?.folder} / {title}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator className="p-0.25 w-full" />
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden min-h-0 p-1">
|
||||||
|
{currentHostConfig?.tunnelConnections &&
|
||||||
|
currentHostConfig.tunnelConnections.length > 0 ? (
|
||||||
|
<div className="rounded-lg h-full overflow-hidden flex flex-col min-h-0">
|
||||||
|
<Tunnel
|
||||||
|
filterHostKey={
|
||||||
|
currentHostConfig?.name &&
|
||||||
|
currentHostConfig.name.trim() !== ""
|
||||||
|
? currentHostConfig.name
|
||||||
|
: `${currentHostConfig?.username}@${currentHostConfig?.ip}`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-gray-400 text-lg">
|
||||||
|
{t("tunnel.noTunnelsConfigured")}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500 text-sm mt-2">
|
||||||
|
{t("tunnel.configureTunnelsInHostSettings")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -43,11 +43,6 @@ export function TunnelViewer({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex flex-col overflow-hidden p-3 min-h-0">
|
<div className="w-full h-full flex flex-col overflow-hidden p-3 min-h-0">
|
||||||
<div className="w-full flex-shrink-0 mb-2">
|
|
||||||
<h1 className="text-xl font-semibold text-foreground">
|
|
||||||
{t("tunnels.title")}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="min-h-0 flex-1 overflow-auto pr-1">
|
<div className="min-h-0 flex-1 overflow-auto pr-1">
|
||||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(320px,1fr))] gap-3 auto-rows-min content-start w-full">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(320px,1fr))] gap-3 auto-rows-min content-start w-full">
|
||||||
{activeHost.tunnelConnections.map((t, idx) => (
|
{activeHost.tunnelConnections.map((t, idx) => (
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React, { useEffect, useRef, useState, useMemo } from "react";
|
import React, { useEffect, useRef, useState, useMemo } from "react";
|
||||||
import { Terminal } from "@/ui/desktop/apps/terminal/Terminal.tsx";
|
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 { 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 { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
|
||||||
import {
|
import {
|
||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
@@ -58,7 +60,9 @@ export function AppView({
|
|||||||
(tab: TabData) =>
|
(tab: TabData) =>
|
||||||
tab.type === "terminal" ||
|
tab.type === "terminal" ||
|
||||||
tab.type === "server" ||
|
tab.type === "server" ||
|
||||||
tab.type === "file_manager",
|
tab.type === "file_manager" ||
|
||||||
|
tab.type === "tunnel" ||
|
||||||
|
tab.type === "docker",
|
||||||
),
|
),
|
||||||
[tabs],
|
[tabs],
|
||||||
);
|
);
|
||||||
@@ -210,7 +214,10 @@ export function AppView({
|
|||||||
const mainTab = terminalTabs.find((tab: TabData) => tab.id === currentTab);
|
const mainTab = terminalTabs.find((tab: TabData) => tab.id === currentTab);
|
||||||
|
|
||||||
if (allSplitScreenTab.length === 0 && mainTab) {
|
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 = {
|
const newStyle = {
|
||||||
position: "absolute" as const,
|
position: "absolute" as const,
|
||||||
top: isFileManagerTab ? 0 : 4,
|
top: isFileManagerTab ? 0 : 4,
|
||||||
@@ -257,9 +264,14 @@ export function AppView({
|
|||||||
const isVisible =
|
const isVisible =
|
||||||
hasStyle || (allSplitScreenTab.length === 0 && t.id === currentTab);
|
hasStyle || (allSplitScreenTab.length === 0 && t.id === currentTab);
|
||||||
|
|
||||||
|
const effectiveVisible = isVisible;
|
||||||
|
|
||||||
const previousStyle = previousStylesRef.current[t.id];
|
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 = {
|
const standardStyle = {
|
||||||
position: "absolute" as const,
|
position: "absolute" as const,
|
||||||
top: isFileManagerTab ? 0 : 4,
|
top: isFileManagerTab ? 0 : 4,
|
||||||
@@ -270,6 +282,16 @@ export function AppView({
|
|||||||
|
|
||||||
const finalStyle: React.CSSProperties = hasStyle
|
const finalStyle: React.CSSProperties = hasStyle
|
||||||
? { ...styles[t.id], overflow: "hidden" }
|
? { ...styles[t.id], overflow: "hidden" }
|
||||||
|
: effectiveVisible
|
||||||
|
? {
|
||||||
|
...(previousStyle || standardStyle),
|
||||||
|
opacity: 1,
|
||||||
|
pointerEvents: "auto",
|
||||||
|
zIndex: 20,
|
||||||
|
display: "block",
|
||||||
|
transition: "opacity 150ms ease-in-out",
|
||||||
|
overflow: "hidden",
|
||||||
|
}
|
||||||
: ({
|
: ({
|
||||||
...(previousStyle || standardStyle),
|
...(previousStyle || standardStyle),
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
@@ -279,8 +301,6 @@ export function AppView({
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
} as React.CSSProperties);
|
} as React.CSSProperties);
|
||||||
|
|
||||||
const effectiveVisible = isVisible;
|
|
||||||
|
|
||||||
const isTerminal = t.type === "terminal";
|
const isTerminal = t.type === "terminal";
|
||||||
const terminalConfig = {
|
const terminalConfig = {
|
||||||
...DEFAULT_TERMINAL_CONFIG,
|
...DEFAULT_TERMINAL_CONFIG,
|
||||||
@@ -317,6 +337,22 @@ export function AppView({
|
|||||||
isTopbarOpen={isTopbarOpen}
|
isTopbarOpen={isTopbarOpen}
|
||||||
embedded
|
embedded
|
||||||
/>
|
/>
|
||||||
|
) : t.type === "tunnel" ? (
|
||||||
|
<TunnelManager
|
||||||
|
hostConfig={t.hostConfig}
|
||||||
|
title={t.title}
|
||||||
|
isVisible={effectiveVisible}
|
||||||
|
isTopbarOpen={isTopbarOpen}
|
||||||
|
embedded
|
||||||
|
/>
|
||||||
|
) : t.type === "docker" ? (
|
||||||
|
<DockerManager
|
||||||
|
hostConfig={t.hostConfig}
|
||||||
|
title={t.title}
|
||||||
|
isVisible={effectiveVisible}
|
||||||
|
isTopbarOpen={isTopbarOpen}
|
||||||
|
embedded
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FileManager
|
<FileManager
|
||||||
embedded
|
embedded
|
||||||
@@ -636,6 +672,8 @@ export function AppView({
|
|||||||
|
|
||||||
const currentTabData = tabs.find((tab: TabData) => tab.id === currentTab);
|
const currentTabData = tabs.find((tab: TabData) => tab.id === currentTab);
|
||||||
const isFileManager = currentTabData?.type === "file_manager";
|
const isFileManager = currentTabData?.type === "file_manager";
|
||||||
|
const isTunnel = currentTabData?.type === "tunnel";
|
||||||
|
const isDocker = currentTabData?.type === "docker";
|
||||||
const isTerminal = currentTabData?.type === "terminal";
|
const isTerminal = currentTabData?.type === "terminal";
|
||||||
const isSplitScreen = allSplitScreenTab.length > 0;
|
const isSplitScreen = allSplitScreenTab.length > 0;
|
||||||
|
|
||||||
@@ -653,7 +691,7 @@ export function AppView({
|
|||||||
const bottomMarginPx = 8;
|
const bottomMarginPx = 8;
|
||||||
|
|
||||||
let containerBackground = "var(--color-dark-bg)";
|
let containerBackground = "var(--color-dark-bg)";
|
||||||
if (isFileManager && !isSplitScreen) {
|
if ((isFileManager || isTunnel || isDocker) && !isSplitScreen) {
|
||||||
containerBackground = "var(--color-dark-bg-darkest)";
|
containerBackground = "var(--color-dark-bg-darkest)";
|
||||||
} else if (isTerminal) {
|
} else if (isTerminal) {
|
||||||
containerBackground = terminalBackgroundColor;
|
containerBackground = terminalBackgroundColor;
|
||||||
|
|||||||
@@ -369,10 +369,13 @@ export function TopNavbar({
|
|||||||
const isTerminal = tab.type === "terminal";
|
const isTerminal = tab.type === "terminal";
|
||||||
const isServer = tab.type === "server";
|
const isServer = tab.type === "server";
|
||||||
const isFileManager = tab.type === "file_manager";
|
const isFileManager = tab.type === "file_manager";
|
||||||
|
const isTunnel = tab.type === "tunnel";
|
||||||
|
const isDocker = tab.type === "docker";
|
||||||
const isSshManager = tab.type === "ssh_manager";
|
const isSshManager = tab.type === "ssh_manager";
|
||||||
const isAdmin = tab.type === "admin";
|
const isAdmin = tab.type === "admin";
|
||||||
const isUserProfile = tab.type === "user_profile";
|
const isUserProfile = tab.type === "user_profile";
|
||||||
const isSplittable = isTerminal || isServer || isFileManager;
|
const isSplittable =
|
||||||
|
isTerminal || isServer || isFileManager || isTunnel || isDocker;
|
||||||
const disableSplit = !isSplittable;
|
const disableSplit = !isSplittable;
|
||||||
const disableActivate =
|
const disableActivate =
|
||||||
isSplit ||
|
isSplit ||
|
||||||
@@ -484,6 +487,8 @@ export function TopNavbar({
|
|||||||
isTerminal ||
|
isTerminal ||
|
||||||
isServer ||
|
isServer ||
|
||||||
isFileManager ||
|
isFileManager ||
|
||||||
|
isTunnel ||
|
||||||
|
isDocker ||
|
||||||
isSshManager ||
|
isSshManager ||
|
||||||
isAdmin ||
|
isAdmin ||
|
||||||
isUserProfile
|
isUserProfile
|
||||||
@@ -498,6 +503,8 @@ export function TopNavbar({
|
|||||||
isTerminal ||
|
isTerminal ||
|
||||||
isServer ||
|
isServer ||
|
||||||
isFileManager ||
|
isFileManager ||
|
||||||
|
isTunnel ||
|
||||||
|
isDocker ||
|
||||||
isSshManager ||
|
isSshManager ||
|
||||||
isAdmin ||
|
isAdmin ||
|
||||||
isUserProfile
|
isUserProfile
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
Server,
|
Server,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Pencil,
|
Pencil,
|
||||||
|
ArrowDownUp,
|
||||||
|
Container,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -63,6 +65,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
|||||||
}, [host.statsConfig]);
|
}, [host.statsConfig]);
|
||||||
|
|
||||||
const shouldShowStatus = statsConfig.statusCheckEnabled !== false;
|
const shouldShowStatus = statsConfig.statusCheckEnabled !== false;
|
||||||
|
const shouldShowMetrics = statsConfig.metricsEnabled !== false;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!shouldShowStatus) {
|
if (!shouldShowStatus) {
|
||||||
@@ -151,6 +154,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
|||||||
side="right"
|
side="right"
|
||||||
className="w-56 bg-dark-bg border-dark-border text-white"
|
className="w-56 bg-dark-bg border-dark-border text-white"
|
||||||
>
|
>
|
||||||
|
{shouldShowMetrics && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
addTab({ type: "server", title, hostConfig: host })
|
addTab({ type: "server", title, hostConfig: host })
|
||||||
@@ -158,8 +162,10 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
|||||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
|
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
|
||||||
>
|
>
|
||||||
<Server className="h-4 w-4" />
|
<Server className="h-4 w-4" />
|
||||||
<span className="flex-1">Open Server Details</span>
|
<span className="flex-1">Open Server Stats</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{host.enableFileManager && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
addTab({ type: "file_manager", title, hostConfig: host })
|
addTab({ type: "file_manager", title, hostConfig: host })
|
||||||
@@ -169,6 +175,29 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
|||||||
<FolderOpen className="h-4 w-4" />
|
<FolderOpen className="h-4 w-4" />
|
||||||
<span className="flex-1">Open File Manager</span>
|
<span className="flex-1">Open File Manager</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{host.enableTunnel && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<ArrowDownUp className="h-4 w-4" />
|
||||||
|
<span className="flex-1">Open Tunnels</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{host.enableDocker && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Container className="h-4 w-4" />
|
||||||
|
<span className="flex-1">Open Docker</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
addTab({
|
addTab({
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
Server as ServerIcon,
|
Server as ServerIcon,
|
||||||
Folder as FolderIcon,
|
Folder as FolderIcon,
|
||||||
User as UserIcon,
|
User as UserIcon,
|
||||||
|
ArrowDownUp as TunnelIcon,
|
||||||
|
Container as DockerIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
interface TabProps {
|
interface TabProps {
|
||||||
@@ -119,10 +121,14 @@ export function Tab({
|
|||||||
tabType === "terminal" ||
|
tabType === "terminal" ||
|
||||||
tabType === "server" ||
|
tabType === "server" ||
|
||||||
tabType === "file_manager" ||
|
tabType === "file_manager" ||
|
||||||
|
tabType === "tunnel" ||
|
||||||
|
tabType === "docker" ||
|
||||||
tabType === "user_profile"
|
tabType === "user_profile"
|
||||||
) {
|
) {
|
||||||
const isServer = tabType === "server";
|
const isServer = tabType === "server";
|
||||||
const isFileManager = tabType === "file_manager";
|
const isFileManager = tabType === "file_manager";
|
||||||
|
const isTunnel = tabType === "tunnel";
|
||||||
|
const isDocker = tabType === "docker";
|
||||||
const isUserProfile = tabType === "user_profile";
|
const isUserProfile = tabType === "user_profile";
|
||||||
|
|
||||||
const displayTitle =
|
const displayTitle =
|
||||||
@@ -131,6 +137,10 @@ export function Tab({
|
|||||||
? t("nav.serverStats")
|
? t("nav.serverStats")
|
||||||
: isFileManager
|
: isFileManager
|
||||||
? t("nav.fileManager")
|
? t("nav.fileManager")
|
||||||
|
: isTunnel
|
||||||
|
? t("nav.tunnels")
|
||||||
|
: isDocker
|
||||||
|
? t("nav.docker")
|
||||||
: isUserProfile
|
: isUserProfile
|
||||||
? t("nav.userProfile")
|
? t("nav.userProfile")
|
||||||
: t("nav.terminal"));
|
: t("nav.terminal"));
|
||||||
@@ -151,6 +161,10 @@ export function Tab({
|
|||||||
<ServerIcon className="h-4 w-4 flex-shrink-0" />
|
<ServerIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
) : isFileManager ? (
|
) : isFileManager ? (
|
||||||
<FolderIcon className="h-4 w-4 flex-shrink-0" />
|
<FolderIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
|
) : isTunnel ? (
|
||||||
|
<TunnelIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
|
) : isDocker ? (
|
||||||
|
<DockerIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
) : isUserProfile ? (
|
) : isUserProfile ? (
|
||||||
<UserIcon className="h-4 w-4 flex-shrink-0" />
|
<UserIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
? t("nav.serverStats")
|
? t("nav.serverStats")
|
||||||
: tabType === "file_manager"
|
: tabType === "file_manager"
|
||||||
? t("nav.fileManager")
|
? t("nav.fileManager")
|
||||||
|
: tabType === "tunnel"
|
||||||
|
? t("nav.tunnels")
|
||||||
|
: tabType === "docker"
|
||||||
|
? t("nav.docker")
|
||||||
: t("nav.terminal");
|
: t("nav.terminal");
|
||||||
const baseTitle = (desiredTitle || defaultTitle).trim();
|
const baseTitle = (desiredTitle || defaultTitle).trim();
|
||||||
const match = baseTitle.match(/^(.*) \((\d+)\)$/);
|
const match = baseTitle.match(/^(.*) \((\d+)\)$/);
|
||||||
@@ -137,7 +141,9 @@ export function TabProvider({ children }: TabProviderProps) {
|
|||||||
const needsUniqueTitle =
|
const needsUniqueTitle =
|
||||||
tabData.type === "terminal" ||
|
tabData.type === "terminal" ||
|
||||||
tabData.type === "server" ||
|
tabData.type === "server" ||
|
||||||
tabData.type === "file_manager";
|
tabData.type === "file_manager" ||
|
||||||
|
tabData.type === "tunnel" ||
|
||||||
|
tabData.type === "docker";
|
||||||
const effectiveTitle = needsUniqueTitle
|
const effectiveTitle = needsUniqueTitle
|
||||||
? computeUniqueTitle(tabData.type, tabData.title)
|
? computeUniqueTitle(tabData.type, tabData.title)
|
||||||
: tabData.title || "";
|
: tabData.title || "";
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
Terminal as TerminalIcon,
|
Terminal as TerminalIcon,
|
||||||
Server as ServerIcon,
|
Server as ServerIcon,
|
||||||
Folder as FolderIcon,
|
Folder as FolderIcon,
|
||||||
|
ArrowDownUp as TunnelIcon,
|
||||||
|
Container as DockerIcon,
|
||||||
Shield as AdminIcon,
|
Shield as AdminIcon,
|
||||||
Network as SshManagerIcon,
|
Network as SshManagerIcon,
|
||||||
User as UserIcon,
|
User as UserIcon,
|
||||||
@@ -33,6 +35,10 @@ export function TabDropdown(): React.ReactElement {
|
|||||||
return <ServerIcon className="h-4 w-4" />;
|
return <ServerIcon className="h-4 w-4" />;
|
||||||
case "file_manager":
|
case "file_manager":
|
||||||
return <FolderIcon className="h-4 w-4" />;
|
return <FolderIcon className="h-4 w-4" />;
|
||||||
|
case "tunnel":
|
||||||
|
return <TunnelIcon className="h-4 w-4" />;
|
||||||
|
case "docker":
|
||||||
|
return <DockerIcon className="h-4 w-4" />;
|
||||||
case "user_profile":
|
case "user_profile":
|
||||||
return <UserIcon className="h-4 w-4" />;
|
return <UserIcon className="h-4 w-4" />;
|
||||||
case "ssh_manager":
|
case "ssh_manager":
|
||||||
@@ -52,6 +58,10 @@ export function TabDropdown(): React.ReactElement {
|
|||||||
return tab.title || t("nav.serverStats");
|
return tab.title || t("nav.serverStats");
|
||||||
case "file_manager":
|
case "file_manager":
|
||||||
return tab.title || t("nav.fileManager");
|
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":
|
case "user_profile":
|
||||||
return tab.title || t("nav.userProfile");
|
return tab.title || t("nav.userProfile");
|
||||||
case "ssh_manager":
|
case "ssh_manager":
|
||||||
|
|||||||
@@ -856,6 +856,7 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
|
|||||||
enableTerminal: Boolean(hostData.enableTerminal),
|
enableTerminal: Boolean(hostData.enableTerminal),
|
||||||
enableTunnel: Boolean(hostData.enableTunnel),
|
enableTunnel: Boolean(hostData.enableTunnel),
|
||||||
enableFileManager: Boolean(hostData.enableFileManager),
|
enableFileManager: Boolean(hostData.enableFileManager),
|
||||||
|
enableDocker: Boolean(hostData.enableDocker),
|
||||||
defaultPath: hostData.defaultPath || "/",
|
defaultPath: hostData.defaultPath || "/",
|
||||||
tunnelConnections: hostData.tunnelConnections || [],
|
tunnelConnections: hostData.tunnelConnections || [],
|
||||||
jumpHosts: hostData.jumpHosts || [],
|
jumpHosts: hostData.jumpHosts || [],
|
||||||
@@ -865,6 +866,11 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
|
|||||||
? hostData.statsConfig
|
? hostData.statsConfig
|
||||||
: JSON.stringify(hostData.statsConfig)
|
: JSON.stringify(hostData.statsConfig)
|
||||||
: null,
|
: null,
|
||||||
|
dockerConfig: hostData.dockerConfig
|
||||||
|
? typeof hostData.dockerConfig === "string"
|
||||||
|
? hostData.dockerConfig
|
||||||
|
: JSON.stringify(hostData.dockerConfig)
|
||||||
|
: null,
|
||||||
terminalConfig: hostData.terminalConfig || null,
|
terminalConfig: hostData.terminalConfig || null,
|
||||||
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
||||||
};
|
};
|
||||||
@@ -922,6 +928,7 @@ export async function updateSSHHost(
|
|||||||
enableTerminal: Boolean(hostData.enableTerminal),
|
enableTerminal: Boolean(hostData.enableTerminal),
|
||||||
enableTunnel: Boolean(hostData.enableTunnel),
|
enableTunnel: Boolean(hostData.enableTunnel),
|
||||||
enableFileManager: Boolean(hostData.enableFileManager),
|
enableFileManager: Boolean(hostData.enableFileManager),
|
||||||
|
enableDocker: Boolean(hostData.enableDocker),
|
||||||
defaultPath: hostData.defaultPath || "/",
|
defaultPath: hostData.defaultPath || "/",
|
||||||
tunnelConnections: hostData.tunnelConnections || [],
|
tunnelConnections: hostData.tunnelConnections || [],
|
||||||
jumpHosts: hostData.jumpHosts || [],
|
jumpHosts: hostData.jumpHosts || [],
|
||||||
@@ -931,6 +938,11 @@ export async function updateSSHHost(
|
|||||||
? hostData.statsConfig
|
? hostData.statsConfig
|
||||||
: JSON.stringify(hostData.statsConfig)
|
: JSON.stringify(hostData.statsConfig)
|
||||||
: null,
|
: null,
|
||||||
|
dockerConfig: hostData.dockerConfig
|
||||||
|
? typeof hostData.dockerConfig === "string"
|
||||||
|
? hostData.dockerConfig
|
||||||
|
: JSON.stringify(hostData.dockerConfig)
|
||||||
|
: null,
|
||||||
terminalConfig: hostData.terminalConfig || null,
|
terminalConfig: hostData.terminalConfig || null,
|
||||||
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user