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" })
|
||||
.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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
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([]),
|
||||
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({
|
||||
<TabsTrigger value="terminal">
|
||||
{t("hosts.terminal")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="docker">Docker</TabsTrigger>
|
||||
<TabsTrigger value="tunnel">{t("hosts.tunnel")}</TabsTrigger>
|
||||
<TabsTrigger value="file_manager">
|
||||
{t("hosts.fileManager")}
|
||||
@@ -2545,6 +2599,307 @@ export function HostManagerEditor({
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</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">
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useSidebar } from "@/components/ui/sidebar.tsx";
|
||||
import { Status, StatusIndicator } from "@/components/ui/shadcn-io/status";
|
||||
import { Separator } from "@/components/ui/separator.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Tunnel } from "@/ui/desktop/apps/tunnel/Tunnel.tsx";
|
||||
import {
|
||||
getServerStatusById,
|
||||
getServerMetricsById,
|
||||
@@ -64,7 +63,7 @@ interface ServerProps {
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
export function Server({
|
||||
export function ServerStats({
|
||||
hostConfig,
|
||||
title,
|
||||
isVisible = true,
|
||||
@@ -462,7 +461,7 @@ export function Server({
|
||||
{(metricsEnabled && showStatsUI) ||
|
||||
(currentHostConfig?.quickActions &&
|
||||
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.length > 0 && (
|
||||
<div className={metricsEnabled && showStatsUI ? "mb-4" : ""}>
|
||||
@@ -600,20 +599,6 @@ export function Server({
|
||||
)}
|
||||
</div>
|
||||
) : 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>
|
||||
@@ -30,7 +30,7 @@ export function CpuWidget({ metrics, metricsHistory }: CpuWidgetProps) {
|
||||
}, [metricsHistory]);
|
||||
|
||||
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">
|
||||
<Cpu className="h-5 w-5 text-blue-400" />
|
||||
<h3 className="font-semibold text-lg text-white">
|
||||
@@ -27,7 +27,7 @@ export function DiskWidget({ metrics }: DiskWidgetProps) {
|
||||
}, [metrics]);
|
||||
|
||||
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">
|
||||
<HardDrive className="h-5 w-5 text-orange-400" />
|
||||
<h3 className="font-semibold text-lg text-white">
|
||||
@@ -35,7 +35,7 @@ export function LoginStatsWidget({ metrics }: LoginStatsWidgetProps) {
|
||||
const uniqueIPs = loginStats?.uniqueIPs || 0;
|
||||
|
||||
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">
|
||||
<UserCheck className="h-5 w-5 text-green-400" />
|
||||
<h3 className="font-semibold text-lg text-white">
|
||||
@@ -30,7 +30,7 @@ export function MemoryWidget({ metrics, metricsHistory }: MemoryWidgetProps) {
|
||||
}, [metricsHistory]);
|
||||
|
||||
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">
|
||||
<MemoryStick className="h-5 w-5 text-green-400" />
|
||||
<h3 className="font-semibold text-lg text-white">
|
||||
@@ -24,7 +24,7 @@ export function NetworkWidget({ metrics }: NetworkWidgetProps) {
|
||||
const interfaces = network?.interfaces || [];
|
||||
|
||||
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">
|
||||
<Network className="h-5 w-5 text-indigo-400" />
|
||||
<h3 className="font-semibold text-lg text-white">
|
||||
@@ -28,7 +28,7 @@ export function ProcessesWidget({ metrics }: ProcessesWidgetProps) {
|
||||
const topProcesses = processes?.top || [];
|
||||
|
||||
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">
|
||||
<List className="h-5 w-5 text-yellow-400" />
|
||||
<h3 className="font-semibold text-lg text-white">
|
||||
@@ -21,7 +21,7 @@ export function SystemWidget({ metrics }: SystemWidgetProps) {
|
||||
const system = metricsWithSystem?.system;
|
||||
|
||||
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">
|
||||
<Server className="h-5 w-5 text-purple-400" />
|
||||
<h3 className="font-semibold text-lg text-white">
|
||||
@@ -20,7 +20,7 @@ export function UptimeWidget({ metrics }: UptimeWidgetProps) {
|
||||
const uptime = metricsWithUptime?.uptime;
|
||||
|
||||
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">
|
||||
<Clock className="h-5 w-5 text-cyan-400" />
|
||||
<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 (
|
||||
<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="grid grid-cols-[repeat(auto-fit,minmax(320px,1fr))] gap-3 auto-rows-min content-start w-full">
|
||||
{activeHost.tunnelConnections.map((t, idx) => (
|
||||
|
||||
@@ -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" ? (
|
||||
<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
|
||||
embedded
|
||||
@@ -636,6 +672,8 @@ export function AppView({
|
||||
|
||||
const currentTabData = tabs.find((tab: TabData) => 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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
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"
|
||||
>
|
||||
<Server className="h-4 w-4" />
|
||||
<span className="flex-1">Open Server Details</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
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"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
<span className="flex-1">Open File Manager</span>
|
||||
</DropdownMenuItem>
|
||||
{shouldShowMetrics && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
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"
|
||||
>
|
||||
<Server className="h-4 w-4" />
|
||||
<span className="flex-1">Open Server Stats</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{host.enableFileManager && (
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
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"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
<span className="flex-1">Open File Manager</span>
|
||||
</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
|
||||
onClick={() =>
|
||||
addTab({
|
||||
|
||||
@@ -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({
|
||||
<ServerIcon className="h-4 w-4 flex-shrink-0" />
|
||||
) : isFileManager ? (
|
||||
<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 ? (
|
||||
<UserIcon className="h-4 w-4 flex-shrink-0" />
|
||||
) : (
|
||||
|
||||
@@ -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 || "";
|
||||
|
||||
@@ -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 <ServerIcon className="h-4 w-4" />;
|
||||
case "file_manager":
|
||||
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":
|
||||
return <UserIcon className="h-4 w-4" />;
|
||||
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":
|
||||
|
||||
@@ -856,6 +856,7 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
|
||||
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<SSHHost> {
|
||||
? 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),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user