feat: added sidebar management and improved some host manager UI/UX
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -514,7 +514,6 @@ export function CredentialEditor({
|
||||
}
|
||||
backgroundColor="var(--bg-base)"
|
||||
/>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit, handleFormError)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)" }}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 || [],
|
||||
|
||||
Reference in New Issue
Block a user