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_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", "public_key", "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" })
.notNull()
.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"),
statsConfig: text("stats_config"),
terminalConfig: text("terminal_config"),

View File

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

View File

@@ -36,7 +36,7 @@ function TooltipTrigger({
function TooltipContent({
className,
sideOffset = 0,
sideOffset = 4,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
@@ -46,7 +46,7 @@ function TooltipContent({
data-slot="tooltip-content"
sideOffset={sideOffset}
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,
)}
{...props}

View File

@@ -1108,6 +1108,19 @@
"quickActionName": "Action name",
"noSnippetFound": "No snippet found",
"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",
"sudoPasswordAutoFill": "Sudo Password Auto-Fill",
"sudoPasswordAutoFillDesc": "Automatically offer to insert SSH password when sudo prompts for password",

View File

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

View File

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

View File

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

View File

@@ -178,17 +178,6 @@ export function CredentialSelector({
</div>
) : (
<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) => (
<Button
key={credential.id}

View File

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

View File

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

View File

@@ -418,6 +418,11 @@ export function HostManagerEditor({
)
.optional(),
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) => {
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>;
@@ -500,6 +520,11 @@ export function HostManagerEditor({
enableTerminal: true,
enableTunnel: true,
enableFileManager: true,
showTerminalInSidebar: true,
showFileManagerInSidebar: false,
showTunnelInSidebar: false,
showDockerInSidebar: false,
showServerStatsInSidebar: false,
defaultPath: "/",
tunnelConnections: [],
jumpHosts: [],
@@ -671,6 +696,11 @@ export function HostManagerEditor({
? cleanedHost.socks5ProxyChain
: [],
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 (
@@ -731,6 +761,11 @@ export function HostManagerEditor({
terminalConfig: DEFAULT_TERMINAL_CONFIG,
forceKeyboardInteractive: false,
enableDocker: false,
showTerminalInSidebar: true,
showFileManagerInSidebar: false,
showTunnelInSidebar: false,
showDockerInSidebar: false,
showServerStatsInSidebar: false,
};
form.reset(defaultFormData as FormData);
@@ -1073,7 +1108,6 @@ export function HostManagerEditor({
}
backgroundColor="var(--bg-base)"
/>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit, handleFormError)}

View File

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

View File

@@ -611,6 +611,132 @@ export function HostGeneralTab({
</Tabs>
<Separator className="my-6" />
<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">
<AccordionTrigger>{t("hosts.advancedAuthSettings")}</AccordionTrigger>
<AccordionContent className="space-y-4 pt-4">

View File

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

View File

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

View File

@@ -959,6 +959,11 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
enableTunnel: Boolean(hostData.enableTunnel),
enableFileManager: Boolean(hostData.enableFileManager),
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 || "/",
tunnelConnections: hostData.tunnelConnections || [],
jumpHosts: hostData.jumpHosts || [],
@@ -1033,6 +1038,11 @@ export async function updateSSHHost(
enableTunnel: Boolean(hostData.enableTunnel),
enableFileManager: Boolean(hostData.enableFileManager),
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 || "/",
tunnelConnections: hostData.tunnelConnections || [],
jumpHosts: hostData.jumpHosts || [],