feat: added sidebar management and improved some host manager UI/UX

This commit is contained in:
LukeGus
2026-01-14 01:23:58 -06:00
parent 264682c5ad
commit dd62b77c79
17 changed files with 546 additions and 129 deletions

View File

@@ -585,6 +585,32 @@ const migrateSchema = () => {
addColumnIfNotExists("ssh_data", "socks5_password", "TEXT"); addColumnIfNotExists("ssh_data", "socks5_password", "TEXT");
addColumnIfNotExists("ssh_data", "socks5_proxy_chain", "TEXT"); addColumnIfNotExists("ssh_data", "socks5_proxy_chain", "TEXT");
addColumnIfNotExists(
"ssh_data",
"show_terminal_in_sidebar",
"INTEGER NOT NULL DEFAULT 1",
);
addColumnIfNotExists(
"ssh_data",
"show_file_manager_in_sidebar",
"INTEGER NOT NULL DEFAULT 0",
);
addColumnIfNotExists(
"ssh_data",
"show_tunnel_in_sidebar",
"INTEGER NOT NULL DEFAULT 0",
);
addColumnIfNotExists(
"ssh_data",
"show_docker_in_sidebar",
"INTEGER NOT NULL DEFAULT 0",
);
addColumnIfNotExists(
"ssh_data",
"show_server_stats_in_sidebar",
"INTEGER NOT NULL DEFAULT 0",
);
addColumnIfNotExists("ssh_credentials", "private_key", "TEXT"); addColumnIfNotExists("ssh_credentials", "private_key", "TEXT");
addColumnIfNotExists("ssh_credentials", "public_key", "TEXT"); addColumnIfNotExists("ssh_credentials", "public_key", "TEXT");
addColumnIfNotExists("ssh_credentials", "detected_key_type", "TEXT"); addColumnIfNotExists("ssh_credentials", "detected_key_type", "TEXT");

View File

@@ -90,6 +90,21 @@ export const sshData = sqliteTable("ssh_data", {
enableDocker: integer("enable_docker", { mode: "boolean" }) enableDocker: integer("enable_docker", { mode: "boolean" })
.notNull() .notNull()
.default(false), .default(false),
showTerminalInSidebar: integer("show_terminal_in_sidebar", { mode: "boolean" })
.notNull()
.default(true),
showFileManagerInSidebar: integer("show_file_manager_in_sidebar", { mode: "boolean" })
.notNull()
.default(false),
showTunnelInSidebar: integer("show_tunnel_in_sidebar", { mode: "boolean" })
.notNull()
.default(false),
showDockerInSidebar: integer("show_docker_in_sidebar", { mode: "boolean" })
.notNull()
.default(false),
showServerStatsInSidebar: integer("show_server_stats_in_sidebar", { mode: "boolean" })
.notNull()
.default(false),
defaultPath: text("default_path"), defaultPath: text("default_path"),
statsConfig: text("stats_config"), statsConfig: text("stats_config"),
terminalConfig: text("terminal_config"), terminalConfig: text("terminal_config"),

View File

@@ -139,6 +139,11 @@ router.get("/db/host/internal", async (req: Request, res: Response) => {
pin: !!host.pin, pin: !!host.pin,
enableTerminal: !!host.enableTerminal, enableTerminal: !!host.enableTerminal,
enableFileManager: !!host.enableFileManager, enableFileManager: !!host.enableFileManager,
showTerminalInSidebar: !!host.showTerminalInSidebar,
showFileManagerInSidebar: !!host.showFileManagerInSidebar,
showTunnelInSidebar: !!host.showTunnelInSidebar,
showDockerInSidebar: !!host.showDockerInSidebar,
showServerStatsInSidebar: !!host.showServerStatsInSidebar,
tags: ["autostart"], tags: ["autostart"],
}; };
}) })
@@ -213,6 +218,11 @@ router.get("/db/host/internal/all", async (req: Request, res: Response) => {
pin: !!host.pin, pin: !!host.pin,
enableTerminal: !!host.enableTerminal, enableTerminal: !!host.enableTerminal,
enableFileManager: !!host.enableFileManager, enableFileManager: !!host.enableFileManager,
showTerminalInSidebar: !!host.showTerminalInSidebar,
showFileManagerInSidebar: !!host.showFileManagerInSidebar,
showTunnelInSidebar: !!host.showTunnelInSidebar,
showDockerInSidebar: !!host.showDockerInSidebar,
showServerStatsInSidebar: !!host.showServerStatsInSidebar,
defaultPath: host.defaultPath, defaultPath: host.defaultPath,
createdAt: host.createdAt, createdAt: host.createdAt,
updatedAt: host.updatedAt, updatedAt: host.updatedAt,
@@ -298,6 +308,11 @@ router.post(
enableTunnel, enableTunnel,
enableFileManager, enableFileManager,
enableDocker, enableDocker,
showTerminalInSidebar,
showFileManagerInSidebar,
showTunnelInSidebar,
showDockerInSidebar,
showServerStatsInSidebar,
defaultPath, defaultPath,
tunnelConnections, tunnelConnections,
jumpHosts, jumpHosts,
@@ -354,6 +369,11 @@ router.post(
: null, : null,
enableFileManager: enableFileManager ? 1 : 0, enableFileManager: enableFileManager ? 1 : 0,
enableDocker: enableDocker ? 1 : 0, enableDocker: enableDocker ? 1 : 0,
showTerminalInSidebar: showTerminalInSidebar ? 1 : 0,
showFileManagerInSidebar: showFileManagerInSidebar ? 1 : 0,
showTunnelInSidebar: showTunnelInSidebar ? 1 : 0,
showDockerInSidebar: showDockerInSidebar ? 1 : 0,
showServerStatsInSidebar: showServerStatsInSidebar ? 1 : 0,
defaultPath: defaultPath || null, defaultPath: defaultPath || null,
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null, statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null, terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
@@ -426,6 +446,11 @@ router.post(
: [], : [],
enableFileManager: !!createdHost.enableFileManager, enableFileManager: !!createdHost.enableFileManager,
enableDocker: !!createdHost.enableDocker, enableDocker: !!createdHost.enableDocker,
showTerminalInSidebar: !!createdHost.showTerminalInSidebar,
showFileManagerInSidebar: !!createdHost.showFileManagerInSidebar,
showTunnelInSidebar: !!createdHost.showTunnelInSidebar,
showDockerInSidebar: !!createdHost.showDockerInSidebar,
showServerStatsInSidebar: !!createdHost.showServerStatsInSidebar,
statsConfig: createdHost.statsConfig statsConfig: createdHost.statsConfig
? JSON.parse(createdHost.statsConfig as string) ? JSON.parse(createdHost.statsConfig as string)
: undefined, : undefined,
@@ -569,6 +594,11 @@ router.put(
enableTunnel, enableTunnel,
enableFileManager, enableFileManager,
enableDocker, enableDocker,
showTerminalInSidebar,
showFileManagerInSidebar,
showTunnelInSidebar,
showDockerInSidebar,
showServerStatsInSidebar,
defaultPath, defaultPath,
tunnelConnections, tunnelConnections,
jumpHosts, jumpHosts,
@@ -626,6 +656,11 @@ router.put(
: null, : null,
enableFileManager: enableFileManager ? 1 : 0, enableFileManager: enableFileManager ? 1 : 0,
enableDocker: enableDocker ? 1 : 0, enableDocker: enableDocker ? 1 : 0,
showTerminalInSidebar: showTerminalInSidebar ? 1 : 0,
showFileManagerInSidebar: showFileManagerInSidebar ? 1 : 0,
showTunnelInSidebar: showTunnelInSidebar ? 1 : 0,
showDockerInSidebar: showDockerInSidebar ? 1 : 0,
showServerStatsInSidebar: showServerStatsInSidebar ? 1 : 0,
defaultPath: defaultPath || null, defaultPath: defaultPath || null,
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null, statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null, terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
@@ -793,6 +828,11 @@ router.put(
: [], : [],
enableFileManager: !!updatedHost.enableFileManager, enableFileManager: !!updatedHost.enableFileManager,
enableDocker: !!updatedHost.enableDocker, enableDocker: !!updatedHost.enableDocker,
showTerminalInSidebar: !!updatedHost.showTerminalInSidebar,
showFileManagerInSidebar: !!updatedHost.showFileManagerInSidebar,
showTunnelInSidebar: !!updatedHost.showTunnelInSidebar,
showDockerInSidebar: !!updatedHost.showDockerInSidebar,
showServerStatsInSidebar: !!updatedHost.showServerStatsInSidebar,
statsConfig: updatedHost.statsConfig statsConfig: updatedHost.statsConfig
? JSON.parse(updatedHost.statsConfig as string) ? JSON.parse(updatedHost.statsConfig as string)
: undefined, : undefined,
@@ -928,6 +968,11 @@ router.get(
quickActions: sshData.quickActions, quickActions: sshData.quickActions,
notes: sshData.notes, notes: sshData.notes,
enableDocker: sshData.enableDocker, enableDocker: sshData.enableDocker,
showTerminalInSidebar: sshData.showTerminalInSidebar,
showFileManagerInSidebar: sshData.showFileManagerInSidebar,
showTunnelInSidebar: sshData.showTunnelInSidebar,
showDockerInSidebar: sshData.showDockerInSidebar,
showServerStatsInSidebar: sshData.showServerStatsInSidebar,
useSocks5: sshData.useSocks5, useSocks5: sshData.useSocks5,
socks5Host: sshData.socks5Host, socks5Host: sshData.socks5Host,
socks5Port: sshData.socks5Port, socks5Port: sshData.socks5Port,
@@ -1017,6 +1062,11 @@ router.get(
: [], : [],
enableFileManager: !!row.enableFileManager, enableFileManager: !!row.enableFileManager,
enableDocker: !!row.enableDocker, enableDocker: !!row.enableDocker,
showTerminalInSidebar: !!row.showTerminalInSidebar,
showFileManagerInSidebar: !!row.showFileManagerInSidebar,
showTunnelInSidebar: !!row.showTunnelInSidebar,
showDockerInSidebar: !!row.showDockerInSidebar,
showServerStatsInSidebar: !!row.showServerStatsInSidebar,
statsConfig: row.statsConfig statsConfig: row.statsConfig
? JSON.parse(row.statsConfig as string) ? JSON.parse(row.statsConfig as string)
: undefined, : undefined,
@@ -1123,6 +1173,11 @@ router.get(
jumpHosts: host.jumpHosts ? JSON.parse(host.jumpHosts) : [], jumpHosts: host.jumpHosts ? JSON.parse(host.jumpHosts) : [],
quickActions: host.quickActions ? JSON.parse(host.quickActions) : [], quickActions: host.quickActions ? JSON.parse(host.quickActions) : [],
enableFileManager: !!host.enableFileManager, enableFileManager: !!host.enableFileManager,
showTerminalInSidebar: !!host.showTerminalInSidebar,
showFileManagerInSidebar: !!host.showFileManagerInSidebar,
showTunnelInSidebar: !!host.showTunnelInSidebar,
showDockerInSidebar: !!host.showDockerInSidebar,
showServerStatsInSidebar: !!host.showServerStatsInSidebar,
statsConfig: host.statsConfig statsConfig: host.statsConfig
? JSON.parse(host.statsConfig) ? JSON.parse(host.statsConfig)
: undefined, : undefined,

View File

@@ -36,7 +36,7 @@ function TooltipTrigger({
function TooltipContent({ function TooltipContent({
className, className,
sideOffset = 0, sideOffset = 4,
children, children,
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) { }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
@@ -46,7 +46,7 @@ function TooltipContent({
data-slot="tooltip-content" data-slot="tooltip-content"
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance", "bg-elevated text-foreground border border-edge-medium shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className, className,
)} )}
{...props} {...props}

View File

@@ -1108,6 +1108,19 @@
"quickActionName": "Action name", "quickActionName": "Action name",
"noSnippetFound": "No snippet found", "noSnippetFound": "No snippet found",
"quickActionsOrder": "Quick action buttons will appear in the order listed above on the Server Stats page", "quickActionsOrder": "Quick action buttons will appear in the order listed above on the Server Stats page",
"sidebarCustomization": "Sidebar Button Customization",
"sidebarCustomizationDesc": "Choose which actions appear as quick buttons in the sidebar. Actions not shown as buttons will appear in the dropdown menu.",
"showTerminalInSidebar": "Show Terminal Button",
"showTerminalInSidebarDesc": "Display terminal as a quick button in the sidebar",
"showFileManagerInSidebar": "Show File Manager Button",
"showFileManagerInSidebarDesc": "Display file manager as a quick button in the sidebar",
"showTunnelInSidebar": "Show Tunnel Button",
"showTunnelInSidebarDesc": "Display tunnel management as a quick button in the sidebar",
"showDockerInSidebar": "Show Docker Button",
"showDockerInSidebarDesc": "Display docker management as a quick button in the sidebar",
"showServerStatsInSidebar": "Show Server Stats Button",
"showServerStatsInSidebarDesc": "Display server statistics as a quick button in the sidebar",
"atLeastOneActionRequired": "At least one enabled action must be shown in the sidebar",
"advancedAuthSettings": "Advanced Authentication Settings", "advancedAuthSettings": "Advanced Authentication Settings",
"sudoPasswordAutoFill": "Sudo Password Auto-Fill", "sudoPasswordAutoFill": "Sudo Password Auto-Fill",
"sudoPasswordAutoFillDesc": "Automatically offer to insert SSH password when sudo prompts for password", "sudoPasswordAutoFillDesc": "Automatically offer to insert SSH password when sudo prompts for password",

View File

@@ -42,6 +42,11 @@ export interface SSHHost {
enableTunnel: boolean; enableTunnel: boolean;
enableFileManager: boolean; enableFileManager: boolean;
enableDocker: boolean; enableDocker: boolean;
showTerminalInSidebar: boolean;
showFileManagerInSidebar: boolean;
showTunnelInSidebar: boolean;
showDockerInSidebar: boolean;
showServerStatsInSidebar: boolean;
defaultPath: string; defaultPath: string;
tunnelConnections: TunnelConnection[]; tunnelConnections: TunnelConnection[];
jumpHosts?: JumpHost[]; jumpHosts?: JumpHost[];
@@ -102,6 +107,11 @@ export interface SSHHostData {
enableTunnel?: boolean; enableTunnel?: boolean;
enableFileManager?: boolean; enableFileManager?: boolean;
enableDocker?: boolean; enableDocker?: boolean;
showTerminalInSidebar?: boolean;
showFileManagerInSidebar?: boolean;
showTunnelInSidebar?: boolean;
showDockerInSidebar?: boolean;
showServerStatsInSidebar?: boolean;
defaultPath?: string; defaultPath?: string;
forceKeyboardInteractive?: boolean; forceKeyboardInteractive?: boolean;
tunnelConnections?: TunnelConnection[]; tunnelConnections?: TunnelConnection[];

View File

@@ -37,6 +37,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button.tsx"; import { Button } from "@/components/ui/button.tsx";
import { ButtonGroup } from "@/components/ui/button-group.tsx";
interface SSHHost { interface SSHHost {
id: number; id: number;
@@ -364,19 +365,90 @@ export function CommandPalette({
}} }}
className="flex items-center justify-between" className="flex items-center justify-between"
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 flex-1 min-w-0">
<Server className="h-4 w-4" /> <Server className="h-4 w-4 flex-shrink-0" />
<span>{title}</span> <span className="truncate">{title}</span>
</div> </div>
<div <ButtonGroup
className="flex items-center gap-1" className="flex-shrink-0"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{host.enableTerminal &&
(host.showTerminalInSidebar ?? true) && (
<Button
variant="outline"
className="!px-2 h-7 border-1 border-edge"
onClick={(e) => {
e.stopPropagation();
handleHostTerminalClick(host);
}}
>
<Terminal className="h-3 w-3" />
</Button>
)}
{host.enableFileManager &&
(host.showFileManagerInSidebar ?? false) && (
<Button
variant="outline"
className="!px-2 h-7 border-1 border-edge"
onClick={(e) => {
e.stopPropagation();
handleHostFileManagerClick(host);
}}
>
<FolderOpen className="h-3 w-3" />
</Button>
)}
{host.enableTunnel &&
hasTunnelConnections &&
(host.showTunnelInSidebar ?? false) && (
<Button
variant="outline"
className="!px-2 h-7 border-1 border-edge"
onClick={(e) => {
e.stopPropagation();
handleHostTunnelClick(host);
}}
>
<ArrowDownUp className="h-3 w-3" />
</Button>
)}
{host.enableDocker &&
(host.showDockerInSidebar ?? false) && (
<Button
variant="outline"
className="!px-2 h-7 border-1 border-edge"
onClick={(e) => {
e.stopPropagation();
handleHostDockerClick(host);
}}
>
<Container className="h-3 w-3" />
</Button>
)}
{shouldShowMetrics &&
(host.showServerStatsInSidebar ?? false) && (
<Button
variant="outline"
className="!px-2 h-7 border-1 border-edge"
onClick={(e) => {
e.stopPropagation();
handleHostServerDetailsClick(host);
}}
>
<Server className="h-3 w-3" />
</Button>
)}
<DropdownMenu modal={false}> <DropdownMenu modal={false}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="outline" variant="outline"
className="!px-2 h-7 border-1 border-edge" className="!px-2 h-7 border-1 border-edge rounded-l-none border-l-0"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<EllipsisVertical className="h-3 w-3" /> <EllipsisVertical className="h-3 w-3" />
@@ -387,62 +459,82 @@ export function CommandPalette({
side="right" side="right"
className="w-56 bg-canvas border-edge text-foreground" className="w-56 bg-canvas border-edge text-foreground"
> >
{shouldShowMetrics && ( {host.enableTerminal &&
<DropdownMenuItem !(host.showTerminalInSidebar ?? true) && (
onClick={(e) => { <DropdownMenuItem
e.stopPropagation(); onClick={(e) => {
handleHostServerDetailsClick(host); e.stopPropagation();
}} handleHostTerminalClick(host);
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary" }}
> className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
<Server className="h-4 w-4" /> >
<span className="flex-1"> <Terminal className="h-4 w-4" />
{t("hosts.openServerStats")} <span className="flex-1">
</span> {t("hosts.openTerminal")}
</DropdownMenuItem> </span>
)} </DropdownMenuItem>
{host.enableFileManager && ( )}
<DropdownMenuItem {shouldShowMetrics &&
onClick={(e) => { !(host.showServerStatsInSidebar ?? false) && (
e.stopPropagation(); <DropdownMenuItem
handleHostFileManagerClick(host); onClick={(e) => {
}} e.stopPropagation();
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary" handleHostServerDetailsClick(host);
> }}
<FolderOpen className="h-4 w-4" /> className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
<span className="flex-1"> >
{t("hosts.openFileManager")} <Server className="h-4 w-4" />
</span> <span className="flex-1">
</DropdownMenuItem> {t("hosts.openServerStats")}
)} </span>
{host.enableTunnel && hasTunnelConnections && ( </DropdownMenuItem>
<DropdownMenuItem )}
onClick={(e) => { {host.enableFileManager &&
e.stopPropagation(); !(host.showFileManagerInSidebar ?? false) && (
handleHostTunnelClick(host); <DropdownMenuItem
}} onClick={(e) => {
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary" e.stopPropagation();
> handleHostFileManagerClick(host);
<ArrowDownUp className="h-4 w-4" /> }}
<span className="flex-1"> className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
{t("hosts.openTunnels")} >
</span> <FolderOpen className="h-4 w-4" />
</DropdownMenuItem> <span className="flex-1">
)} {t("hosts.openFileManager")}
{host.enableDocker && ( </span>
<DropdownMenuItem </DropdownMenuItem>
onClick={(e) => { )}
e.stopPropagation(); {host.enableTunnel &&
handleHostDockerClick(host); hasTunnelConnections &&
}} !(host.showTunnelInSidebar ?? false) && (
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary" <DropdownMenuItem
> onClick={(e) => {
<Container className="h-4 w-4" /> e.stopPropagation();
<span className="flex-1"> handleHostTunnelClick(host);
{t("hosts.openDocker")} }}
</span> className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
</DropdownMenuItem> >
)} <ArrowDownUp className="h-4 w-4" />
<span className="flex-1">
{t("hosts.openTunnels")}
</span>
</DropdownMenuItem>
)}
{host.enableDocker &&
!(host.showDockerInSidebar ?? false) && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleHostDockerClick(host);
}}
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
>
<Container className="h-4 w-4" />
<span className="flex-1">
{t("hosts.openDocker")}
</span>
</DropdownMenuItem>
)}
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -455,7 +547,7 @@ export function CommandPalette({
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </ButtonGroup>
</CommandItem> </CommandItem>
); );
})} })}

View File

@@ -514,7 +514,6 @@ export function CredentialEditor({
} }
backgroundColor="var(--bg-base)" backgroundColor="var(--bg-base)"
/> />
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit, handleFormError)} onSubmit={form.handleSubmit(onSubmit, handleFormError)}

View File

@@ -178,17 +178,6 @@ export function CredentialSelector({
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 gap-2.5"> <div className="grid grid-cols-1 gap-2.5">
{value && (
<Button
type="button"
variant="ghost"
size="sm"
className="w-full justify-start text-left rounded-lg px-3 py-2 text-destructive hover:bg-destructive/10 transition-colors duration-200"
onClick={handleClear}
>
{t("common.clear")}
</Button>
)}
{filteredCredentials.map((credential) => ( {filteredCredentials.map((credential) => (
<Button <Button
key={credential.id} key={credential.id}

View File

@@ -758,15 +758,6 @@ export function CredentialsManager({
)} )}
{credential.authType} {credential.authType}
</Badge> </Badge>
{credential.authType === "key" &&
credential.keyType && (
<Badge
variant="outline"
className="text-xs px-1 py-0"
>
{credential.keyType}
</Badge>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -112,6 +112,7 @@ export function HostManager({
const handleTabChange = (value: string) => { const handleTabChange = (value: string) => {
if (activeTab === "add_host" && value !== "add_host") { if (activeTab === "add_host" && value !== "add_host") {
setEditingHost(null); setEditingHost(null);
lastProcessedHostIdRef.current = undefined;
} }
if (activeTab === "add_credential" && value !== "add_credential") { if (activeTab === "add_credential" && value !== "add_credential") {
setEditingCredential(null); setEditingCredential(null);

View File

@@ -418,6 +418,11 @@ export function HostManagerEditor({
) )
.optional(), .optional(),
enableDocker: z.boolean().default(false), enableDocker: z.boolean().default(false),
showTerminalInSidebar: z.boolean().default(true),
showFileManagerInSidebar: z.boolean().default(false),
showTunnelInSidebar: z.boolean().default(false),
showDockerInSidebar: z.boolean().default(false),
showServerStatsInSidebar: z.boolean().default(false),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
if (data.authType === "none") { if (data.authType === "none") {
@@ -475,6 +480,21 @@ export function HostManagerEditor({
}); });
} }
}); });
const hasAtLeastOneSidebarAction =
(data.enableTerminal && data.showTerminalInSidebar) ||
(data.enableFileManager && data.showFileManagerInSidebar) ||
(data.enableTunnel && data.showTunnelInSidebar) ||
(data.enableDocker && data.showDockerInSidebar) ||
data.showServerStatsInSidebar;
if (!hasAtLeastOneSidebarAction) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("hosts.atLeastOneActionRequired"),
path: ["showTerminalInSidebar"],
});
}
}); });
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
@@ -500,6 +520,11 @@ export function HostManagerEditor({
enableTerminal: true, enableTerminal: true,
enableTunnel: true, enableTunnel: true,
enableFileManager: true, enableFileManager: true,
showTerminalInSidebar: true,
showFileManagerInSidebar: false,
showTunnelInSidebar: false,
showDockerInSidebar: false,
showServerStatsInSidebar: false,
defaultPath: "/", defaultPath: "/",
tunnelConnections: [], tunnelConnections: [],
jumpHosts: [], jumpHosts: [],
@@ -671,6 +696,11 @@ export function HostManagerEditor({
? cleanedHost.socks5ProxyChain ? cleanedHost.socks5ProxyChain
: [], : [],
enableDocker: Boolean(cleanedHost.enableDocker), enableDocker: Boolean(cleanedHost.enableDocker),
showTerminalInSidebar: cleanedHost.showTerminalInSidebar ?? true,
showFileManagerInSidebar: cleanedHost.showFileManagerInSidebar ?? false,
showTunnelInSidebar: cleanedHost.showTunnelInSidebar ?? false,
showDockerInSidebar: cleanedHost.showDockerInSidebar ?? false,
showServerStatsInSidebar: cleanedHost.showServerStatsInSidebar ?? false,
}; };
if ( if (
@@ -731,6 +761,11 @@ export function HostManagerEditor({
terminalConfig: DEFAULT_TERMINAL_CONFIG, terminalConfig: DEFAULT_TERMINAL_CONFIG,
forceKeyboardInteractive: false, forceKeyboardInteractive: false,
enableDocker: false, enableDocker: false,
showTerminalInSidebar: true,
showFileManagerInSidebar: false,
showTunnelInSidebar: false,
showDockerInSidebar: false,
showServerStatsInSidebar: false,
}; };
form.reset(defaultFormData as FormData); form.reset(defaultFormData as FormData);
@@ -1073,7 +1108,6 @@ export function HostManagerEditor({
} }
backgroundColor="var(--bg-base)" backgroundColor="var(--bg-base)"
/> />
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit, handleFormError)} onSubmit={form.handleSubmit(onSubmit, handleFormError)}

View File

@@ -868,10 +868,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
{importing ? t("hosts.importing") : t("hosts.importJson")} {importing ? t("hosts.importing") : t("hosts.importJson")}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent <TooltipContent side="bottom" className="max-w-sm">
side="bottom"
className="max-w-sm bg-popover text-popover-foreground border border-border shadow-lg"
>
<div className="space-y-2"> <div className="space-y-2">
<p className="font-semibold text-sm"> <p className="font-semibold text-sm">
{t("hosts.importJsonTitle")} {t("hosts.importJsonTitle")}
@@ -952,10 +949,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
{importing ? t("hosts.importing") : t("hosts.importJson")} {importing ? t("hosts.importing") : t("hosts.importJson")}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent <TooltipContent side="bottom" className="max-w-sm">
side="bottom"
className="max-w-sm bg-popover text-popover-foreground border border-border shadow-lg"
>
<div className="space-y-2"> <div className="space-y-2">
<p className="font-semibold text-sm"> <p className="font-semibold text-sm">
{t("hosts.importJsonTitle")} {t("hosts.importJsonTitle")}

View File

@@ -611,6 +611,132 @@ export function HostGeneralTab({
</Tabs> </Tabs>
<Separator className="my-6" /> <Separator className="my-6" />
<Accordion type="multiple" className="w-full"> <Accordion type="multiple" className="w-full">
<AccordionItem value="sidebar-customization">
<AccordionTrigger>{t("hosts.sidebarCustomization")}</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">
<Alert>
<AlertDescription>
{t("hosts.sidebarCustomizationDesc")}
</AlertDescription>
</Alert>
{form.watch("enableTerminal") && (
<FormField
control={form.control}
name="showTerminalInSidebar"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
<div className="space-y-0.5">
<FormLabel>{t("hosts.showTerminalInSidebar")}</FormLabel>
<FormDescription>
{t("hosts.showTerminalInSidebarDesc")}
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
{form.watch("enableFileManager") && (
<FormField
control={form.control}
name="showFileManagerInSidebar"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
<div className="space-y-0.5">
<FormLabel>
{t("hosts.showFileManagerInSidebar")}
</FormLabel>
<FormDescription>
{t("hosts.showFileManagerInSidebarDesc")}
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
{form.watch("enableTunnel") && (
<FormField
control={form.control}
name="showTunnelInSidebar"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
<div className="space-y-0.5">
<FormLabel>{t("hosts.showTunnelInSidebar")}</FormLabel>
<FormDescription>
{t("hosts.showTunnelInSidebarDesc")}
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
{form.watch("enableDocker") && (
<FormField
control={form.control}
name="showDockerInSidebar"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
<div className="space-y-0.5">
<FormLabel>{t("hosts.showDockerInSidebar")}</FormLabel>
<FormDescription>
{t("hosts.showDockerInSidebarDesc")}
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="showServerStatsInSidebar"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
<div className="space-y-0.5">
<FormLabel>{t("hosts.showServerStatsInSidebar")}</FormLabel>
<FormDescription>
{t("hosts.showServerStatsInSidebarDesc")}
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</AccordionContent>
</AccordionItem>
<AccordionItem value="advanced-auth"> <AccordionItem value="advanced-auth">
<AccordionTrigger>{t("hosts.advancedAuthSettings")}</AccordionTrigger> <AccordionTrigger>{t("hosts.advancedAuthSettings")}</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4"> <AccordionContent className="space-y-4 pt-4">

View File

@@ -44,7 +44,7 @@ export function SimpleLoader({
<div <div
className={cn( className={cn(
"absolute inset-0 flex items-center justify-center z-50", "absolute inset-0 flex items-center justify-center z-[100]",
className, className,
)} )}
style={{ backgroundColor: backgroundColor || "var(--bg-base)" }} style={{ backgroundColor: backgroundColor || "var(--bg-base)" }}

View File

@@ -155,7 +155,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
</p> </p>
<ButtonGroup className="flex-shrink-0"> <ButtonGroup className="flex-shrink-0">
{host.enableTerminal && ( {host.enableTerminal && (host.showTerminalInSidebar ?? true) && (
<Button <Button
variant="outline" variant="outline"
className="!px-2 border-1 border-edge" className="!px-2 border-1 border-edge"
@@ -165,13 +165,62 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
</Button> </Button>
)} )}
{host.enableFileManager &&
(host.showFileManagerInSidebar ?? false) && (
<Button
variant="outline"
className="!px-2 border-1 border-edge"
onClick={() =>
addTab({ type: "file_manager", title, hostConfig: host })
}
>
<FolderOpen />
</Button>
)}
{host.enableTunnel &&
hasTunnelConnections &&
(host.showTunnelInSidebar ?? false) && (
<Button
variant="outline"
className="!px-2 border-1 border-edge"
onClick={() =>
addTab({ type: "tunnel", title, hostConfig: host })
}
>
<ArrowDownUp />
</Button>
)}
{host.enableDocker && (host.showDockerInSidebar ?? false) && (
<Button
variant="outline"
className="!px-2 border-1 border-edge"
onClick={() =>
addTab({ type: "docker", title, hostConfig: host })
}
>
<Container />
</Button>
)}
{shouldShowMetrics && (host.showServerStatsInSidebar ?? false) && (
<Button
variant="outline"
className="!px-2 border-1 border-edge"
onClick={() =>
addTab({ type: "server_stats", title, hostConfig: host })
}
>
<Server />
</Button>
)}
<DropdownMenu modal={false}> <DropdownMenu modal={false}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="outline" variant="outline"
className={`!px-2 border-1 border-edge ${ className="!px-2 border-1 border-edge rounded-l-none border-l-0"
host.enableTerminal ? "rounded-tl-none rounded-bl-none" : ""
}`}
> >
<EllipsisVertical /> <EllipsisVertical />
</Button> </Button>
@@ -182,40 +231,53 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
side="right" side="right"
className="w-56 bg-canvas border-edge text-foreground" className="w-56 bg-canvas border-edge text-foreground"
> >
{shouldShowMetrics && ( {host.enableTerminal && !(host.showTerminalInSidebar ?? true) && (
<DropdownMenuItem <DropdownMenuItem
onClick={() => onClick={handleTerminalClick}
addTab({ type: "server_stats", title, hostConfig: host })
}
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary" className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
> >
<Server className="h-4 w-4" /> <Terminal className="h-4 w-4" />
<span className="flex-1">{t("hosts.openServerStats")}</span> <span className="flex-1">{t("hosts.openTerminal")}</span>
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{host.enableFileManager && ( {shouldShowMetrics &&
<DropdownMenuItem !(host.showServerStatsInSidebar ?? false) && (
onClick={() => <DropdownMenuItem
addTab({ type: "file_manager", title, hostConfig: host }) onClick={() =>
} addTab({ type: "server_stats", title, hostConfig: host })
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary" }
> className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
<FolderOpen className="h-4 w-4" /> >
<span className="flex-1">{t("hosts.openFileManager")}</span> <Server className="h-4 w-4" />
</DropdownMenuItem> <span className="flex-1">{t("hosts.openServerStats")}</span>
)} </DropdownMenuItem>
{host.enableTunnel && hasTunnelConnections && ( )}
<DropdownMenuItem {host.enableFileManager &&
onClick={() => !(host.showFileManagerInSidebar ?? false) && (
addTab({ type: "tunnel", title, hostConfig: host }) <DropdownMenuItem
} onClick={() =>
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary" addTab({ type: "file_manager", title, hostConfig: host })
> }
<ArrowDownUp className="h-4 w-4" /> className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
<span className="flex-1">{t("hosts.openTunnels")}</span> >
</DropdownMenuItem> <FolderOpen className="h-4 w-4" />
)} <span className="flex-1">{t("hosts.openFileManager")}</span>
{host.enableDocker && ( </DropdownMenuItem>
)}
{host.enableTunnel &&
hasTunnelConnections &&
!(host.showTunnelInSidebar ?? false) && (
<DropdownMenuItem
onClick={() =>
addTab({ type: "tunnel", title, hostConfig: host })
}
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-hover text-foreground-secondary"
>
<ArrowDownUp className="h-4 w-4" />
<span className="flex-1">{t("hosts.openTunnels")}</span>
</DropdownMenuItem>
)}
{host.enableDocker && !(host.showDockerInSidebar ?? false) && (
<DropdownMenuItem <DropdownMenuItem
onClick={() => onClick={() =>
addTab({ type: "docker", title, hostConfig: host }) addTab({ type: "docker", title, hostConfig: host })

View File

@@ -959,6 +959,11 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
enableTunnel: Boolean(hostData.enableTunnel), enableTunnel: Boolean(hostData.enableTunnel),
enableFileManager: Boolean(hostData.enableFileManager), enableFileManager: Boolean(hostData.enableFileManager),
enableDocker: Boolean(hostData.enableDocker), enableDocker: Boolean(hostData.enableDocker),
showTerminalInSidebar: Boolean(hostData.showTerminalInSidebar),
showFileManagerInSidebar: Boolean(hostData.showFileManagerInSidebar),
showTunnelInSidebar: Boolean(hostData.showTunnelInSidebar),
showDockerInSidebar: Boolean(hostData.showDockerInSidebar),
showServerStatsInSidebar: Boolean(hostData.showServerStatsInSidebar),
defaultPath: hostData.defaultPath || "/", defaultPath: hostData.defaultPath || "/",
tunnelConnections: hostData.tunnelConnections || [], tunnelConnections: hostData.tunnelConnections || [],
jumpHosts: hostData.jumpHosts || [], jumpHosts: hostData.jumpHosts || [],
@@ -1033,6 +1038,11 @@ export async function updateSSHHost(
enableTunnel: Boolean(hostData.enableTunnel), enableTunnel: Boolean(hostData.enableTunnel),
enableFileManager: Boolean(hostData.enableFileManager), enableFileManager: Boolean(hostData.enableFileManager),
enableDocker: Boolean(hostData.enableDocker), enableDocker: Boolean(hostData.enableDocker),
showTerminalInSidebar: Boolean(hostData.showTerminalInSidebar),
showFileManagerInSidebar: Boolean(hostData.showFileManagerInSidebar),
showTunnelInSidebar: Boolean(hostData.showTunnelInSidebar),
showDockerInSidebar: Boolean(hostData.showDockerInSidebar),
showServerStatsInSidebar: Boolean(hostData.showServerStatsInSidebar),
defaultPath: hostData.defaultPath || "/", defaultPath: hostData.defaultPath || "/",
tunnelConnections: hostData.tunnelConnections || [], tunnelConnections: hostData.tunnelConnections || [],
jumpHosts: hostData.jumpHosts || [], jumpHosts: hostData.jumpHosts || [],