import { zodResolver } from "@hookform/resolvers/zod"; import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button.tsx"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, } from "@/components/ui/form.tsx"; import { Input } from "@/components/ui/input.tsx"; import { PasswordInput } from "@/components/ui/password-input.tsx"; import { ScrollArea } from "@/components/ui/scroll-area.tsx"; import { Separator } from "@/components/ui/separator.tsx"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/components/ui/tabs.tsx"; import React, { useEffect, useRef, useState } from "react"; import { Switch } from "@/components/ui/switch.tsx"; import { Alert, AlertDescription } from "@/components/ui/alert.tsx"; import { toast } from "sonner"; import { createSSHHost, getCredentials, getSSHHosts, updateSSHHost, enableAutoStart, disableAutoStart, getSnippets, } from "@/ui/main-axios.ts"; import { useTranslation } from "react-i18next"; import { CredentialSelector } from "@/ui/desktop/apps/credentials/CredentialSelector.tsx"; import CodeMirror from "@uiw/react-codemirror"; import { oneDark } from "@codemirror/theme-one-dark"; import { EditorView } from "@codemirror/view"; import type { StatsConfig } from "@/types/stats-widgets"; import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets"; import { Checkbox } from "@/components/ui/checkbox.tsx"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select.tsx"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, } from "@/components/ui/command.tsx"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover.tsx"; import { Slider } from "@/components/ui/slider.tsx"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion.tsx"; import { TERMINAL_THEMES, TERMINAL_FONTS, CURSOR_STYLES, BELL_STYLES, FAST_SCROLL_MODIFIERS, DEFAULT_TERMINAL_CONFIG, } from "@/constants/terminal-themes"; import { TerminalPreview } from "@/ui/desktop/apps/terminal/TerminalPreview.tsx"; import type { TerminalConfig, SSHHost } from "@/types"; import { Plus, X, Check, ChevronsUpDown } from "lucide-react"; interface JumpHostItemProps { jumpHost: { hostId: number }; index: number; hosts: SSHHost[]; editingHost?: SSHHost | null; onUpdate: (hostId: number) => void; onRemove: () => void; t: (key: string) => string; } function JumpHostItem({ jumpHost, index, hosts, editingHost, onUpdate, onRemove, t, }: JumpHostItemProps) { const [open, setOpen] = React.useState(false); const selectedHost = hosts.find((h) => h.id === jumpHost.hostId); return (
{index + 1}. {t("hosts.noServerFound")} {hosts .filter((h) => !editingHost || h.id !== editingHost.id) .map((host) => ( { onUpdate(host.id); setOpen(false); }} >
{host.name || `${host.username}@${host.ip}`} {host.username}@{host.ip}:{host.port}
))}
); } interface QuickActionItemProps { quickAction: { name: string; snippetId: number }; index: number; snippets: Array<{ id: number; name: string; content: string }>; onUpdate: (name: string, snippetId: number) => void; onRemove: () => void; t: (key: string) => string; } function QuickActionItem({ quickAction, index, snippets, onUpdate, onRemove, t, }: QuickActionItemProps) { const [open, setOpen] = React.useState(false); const selectedSnippet = snippets.find((s) => s.id === quickAction.snippetId); return (
{index + 1}. onUpdate(e.target.value, quickAction.snippetId)} className="flex-1" />
{t("hosts.noSnippetFound")} {snippets.map((snippet) => ( { onUpdate(quickAction.name, snippet.id); setOpen(false); }} >
{snippet.name} {snippet.content}
))}
); } interface SSHManagerHostEditorProps { editingHost?: SSHHost | null; onFormSubmit?: (updatedHost?: SSHHost) => void; } export function HostManagerEditor({ editingHost, onFormSubmit, }: SSHManagerHostEditorProps) { const { t } = useTranslation(); const [folders, setFolders] = useState([]); const [sshConfigurations, setSshConfigurations] = useState([]); const [hosts, setHosts] = useState([]); const [credentials, setCredentials] = useState< Array<{ id: number; username: string; authType: string }> >([]); const [snippets, setSnippets] = useState< Array<{ id: number; name: string; content: string }> >([]); const [authTab, setAuthTab] = useState< "password" | "key" | "credential" | "none" >("password"); const [keyInputMethod, setKeyInputMethod] = useState<"upload" | "paste">( "upload", ); const isSubmittingRef = useRef(false); const [activeTab, setActiveTab] = useState("general"); const [formError, setFormError] = useState(null); useEffect(() => { setFormError(null); }, [activeTab]); const [statusIntervalUnit, setStatusIntervalUnit] = useState< "seconds" | "minutes" >("seconds"); const [metricsIntervalUnit, setMetricsIntervalUnit] = useState< "seconds" | "minutes" >("seconds"); const ipInputRef = useRef(null); useEffect(() => { const fetchData = async () => { try { const [hostsData, credentialsData, snippetsData] = await Promise.all([ getSSHHosts(), getCredentials(), getSnippets(), ]); setHosts(hostsData); setCredentials(credentialsData); setSnippets(Array.isArray(snippetsData) ? snippetsData : []); const uniqueFolders = [ ...new Set( hostsData .filter((host) => host.folder && host.folder.trim() !== "") .map((host) => host.folder), ), ].sort(); const uniqueConfigurations = [ ...new Set( hostsData .filter((host) => host.name && host.name.trim() !== "") .map((host) => host.name), ), ].sort(); setFolders(uniqueFolders); setSshConfigurations(uniqueConfigurations); } catch (error) { console.error("Host manager operation failed:", error); } }; fetchData(); }, []); useEffect(() => { const handleCredentialChange = async () => { try { const hostsData = await getSSHHosts(); const uniqueFolders = [ ...new Set( hostsData .filter((host) => host.folder && host.folder.trim() !== "") .map((host) => host.folder), ), ].sort(); const uniqueConfigurations = [ ...new Set( hostsData .filter((host) => host.name && host.name.trim() !== "") .map((host) => host.name), ), ].sort(); setFolders(uniqueFolders); setSshConfigurations(uniqueConfigurations); } catch (error) { console.error("Host manager operation failed:", error); } }; window.addEventListener("credentials:changed", handleCredentialChange); return () => { window.removeEventListener("credentials:changed", handleCredentialChange); }; }, []); const formSchema = z .object({ connectionType: z.enum(["ssh", "rdp", "vnc", "telnet"]).default("ssh"), name: z.string().optional(), ip: z.string().min(1), port: z.coerce.number().min(1).max(65535), username: z.string().min(1), folder: z.string().optional(), tags: z.array(z.string().min(1)).default([]), pin: z.boolean().default(false), authType: z.enum(["password", "key", "credential", "none"]), // RDP/VNC specific fields domain: z.string().optional(), security: z.string().optional(), ignoreCert: z.boolean().default(false), // RDP/VNC extended configuration guacamoleConfig: z.object({ // Display settings colorDepth: z.coerce.number().optional(), width: z.coerce.number().optional(), height: z.coerce.number().optional(), dpi: z.coerce.number().optional(), resizeMethod: z.string().optional(), forceLossless: z.boolean().optional(), // Audio settings disableAudio: z.boolean().optional(), enableAudioInput: z.boolean().optional(), // RDP Performance settings enableWallpaper: z.boolean().optional(), enableTheming: z.boolean().optional(), enableFontSmoothing: z.boolean().optional(), enableFullWindowDrag: z.boolean().optional(), enableDesktopComposition: z.boolean().optional(), enableMenuAnimations: z.boolean().optional(), disableBitmapCaching: z.boolean().optional(), disableOffscreenCaching: z.boolean().optional(), disableGlyphCaching: z.boolean().optional(), disableGfx: z.boolean().optional(), // RDP Device redirection enablePrinting: z.boolean().optional(), printerName: z.string().optional(), enableDrive: z.boolean().optional(), driveName: z.string().optional(), drivePath: z.string().optional(), createDrivePath: z.boolean().optional(), disableDownload: z.boolean().optional(), disableUpload: z.boolean().optional(), enableTouch: z.boolean().optional(), // RDP Session settings clientName: z.string().optional(), console: z.boolean().optional(), initialProgram: z.string().optional(), serverLayout: z.string().optional(), timezone: z.string().optional(), // RDP Gateway settings gatewayHostname: z.string().optional(), gatewayPort: z.coerce.number().optional(), gatewayUsername: z.string().optional(), gatewayPassword: z.string().optional(), gatewayDomain: z.string().optional(), // RDP RemoteApp settings remoteApp: z.string().optional(), remoteAppDir: z.string().optional(), remoteAppArgs: z.string().optional(), // Clipboard settings normalizeClipboard: z.string().optional(), disableCopy: z.boolean().optional(), disablePaste: z.boolean().optional(), // VNC specific settings cursor: z.string().optional(), swapRedBlue: z.boolean().optional(), readOnly: z.boolean().optional(), // Recording settings recordingPath: z.string().optional(), recordingName: z.string().optional(), createRecordingPath: z.boolean().optional(), recordingExcludeOutput: z.boolean().optional(), recordingExcludeMouse: z.boolean().optional(), recordingIncludeKeys: z.boolean().optional(), // Wake-on-LAN settings wolSendPacket: z.boolean().optional(), wolMacAddr: z.string().optional(), wolBroadcastAddr: z.string().optional(), wolUdpPort: z.coerce.number().optional(), wolWaitTime: z.coerce.number().optional(), }).optional(), credentialId: z.number().optional().nullable(), overrideCredentialUsername: z.boolean().optional(), password: z.string().optional(), key: z.any().optional().nullable(), keyPassword: z.string().optional(), keyType: z .enum([ "auto", "ssh-rsa", "ssh-ed25519", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", "ssh-dss", "ssh-rsa-sha2-256", "ssh-rsa-sha2-512", ]) .optional(), enableTerminal: z.boolean().default(true), enableTunnel: z.boolean().default(true), tunnelConnections: z .array( z.object({ sourcePort: z.coerce.number().min(1).max(65535), endpointPort: z.coerce.number().min(1).max(65535), endpointHost: z.string().min(1), maxRetries: z.coerce.number().min(0).max(100).default(3), retryInterval: z.coerce.number().min(1).max(3600).default(10), autoStart: z.boolean().default(false), }), ) .default([]), enableFileManager: z.boolean().default(true), defaultPath: z.string().optional(), statsConfig: z .object({ enabledWidgets: z .array( z.enum([ "cpu", "memory", "disk", "network", "uptime", "processes", "system", "login_stats", ]), ) .default([ "cpu", "memory", "disk", "network", "uptime", "system", "login_stats", ]), statusCheckEnabled: z.boolean().default(true), statusCheckInterval: z.number().min(5).max(3600).default(30), metricsEnabled: z.boolean().default(true), metricsInterval: z.number().min(5).max(3600).default(30), }) .default({ enabledWidgets: [ "cpu", "memory", "disk", "network", "uptime", "system", "login_stats", ], statusCheckEnabled: true, statusCheckInterval: 30, metricsEnabled: true, metricsInterval: 30, }), terminalConfig: z .object({ cursorBlink: z.boolean(), cursorStyle: z.enum(["block", "underline", "bar"]), fontSize: z.number().min(8).max(24), fontFamily: z.string(), letterSpacing: z.number().min(-2).max(10), lineHeight: z.number().min(1.0).max(2.0), theme: z.string(), scrollback: z.number().min(1000).max(50000), bellStyle: z.enum(["none", "sound", "visual", "both"]), rightClickSelectsWord: z.boolean(), fastScrollModifier: z.enum(["alt", "ctrl", "shift"]), fastScrollSensitivity: z.number().min(1).max(10), minimumContrastRatio: z.number().min(1).max(21), backspaceMode: z.enum(["normal", "control-h"]), agentForwarding: z.boolean(), environmentVariables: z.array( z.object({ key: z.string(), value: z.string(), }), ), startupSnippetId: z.number().nullable(), autoMosh: z.boolean(), moshCommand: z.string(), sudoPasswordAutoFill: z.boolean(), }) .optional(), forceKeyboardInteractive: z.boolean().optional(), jumpHosts: z .array( z.object({ hostId: z.number().min(1), }), ) .default([]), quickActions: z .array( z.object({ name: z.string().min(1), snippetId: z.number().min(1), }), ) .default([]), enableDocker: z.boolean().default(false), dockerConfig: z .object({ connectionType: z.enum(["socket", "tcp", "tls"]).default("socket"), socketPath: z.string().optional(), host: z.string().optional(), port: z.coerce.number().min(1).max(65535).optional(), tlsVerify: z.boolean().default(true), tlsCaCert: z.string().optional(), tlsCert: z.string().optional(), tlsKey: z.string().optional(), }) .optional(), }) .superRefine((data, ctx) => { if (data.authType === "none") { return; } if (data.authType === "password") { if ( !data.password || (typeof data.password === "string" && data.password.trim() === "") ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: t("hosts.passwordRequired"), path: ["password"], }); } } else if (data.authType === "key") { if ( !data.key || (typeof data.key === "string" && data.key.trim() === "") ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: t("hosts.sshKeyRequired"), path: ["key"], }); } if (!data.keyType) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: t("hosts.keyTypeRequired"), path: ["keyType"], }); } } else if (data.authType === "credential") { if ( !data.credentialId || (typeof data.credentialId === "string" && data.credentialId.trim() === "") ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: t("hosts.credentialRequired"), path: ["credentialId"], }); } } data.tunnelConnections.forEach((connection, index) => { if ( connection.endpointHost && !sshConfigurations.includes(connection.endpointHost) ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: t("hosts.mustSelectValidSshConfig"), path: ["tunnelConnections", index, "endpointHost"], }); } }); }); type FormData = z.infer; const form = useForm({ resolver: zodResolver(formSchema) as any, defaultValues: { name: "", connectionType: "ssh" as const, ip: "", port: 22, username: "", folder: "", tags: [], pin: false, authType: "password" as const, credentialId: null, overrideCredentialUsername: false, password: "", key: null, keyPassword: "", keyType: "auto" as const, enableTerminal: true, enableTunnel: true, enableFileManager: true, defaultPath: "/", tunnelConnections: [], jumpHosts: [], quickActions: [], statsConfig: DEFAULT_STATS_CONFIG, terminalConfig: DEFAULT_TERMINAL_CONFIG, forceKeyboardInteractive: false, enableDocker: false, dockerConfig: { connectionType: "socket" as const, socketPath: "/var/run/docker.sock", host: "", port: 2375, tlsVerify: true, tlsCaCert: "", tlsCert: "", tlsKey: "", }, // RDP/VNC specific defaults domain: "", security: "", ignoreCert: false, guacamoleConfig: { // Display settings colorDepth: undefined, width: undefined, height: undefined, dpi: 96, resizeMethod: "display-update", forceLossless: false, // Audio settings disableAudio: false, enableAudioInput: false, // RDP Performance settings enableWallpaper: false, enableTheming: false, enableFontSmoothing: true, enableFullWindowDrag: false, enableDesktopComposition: false, enableMenuAnimations: false, disableBitmapCaching: false, disableOffscreenCaching: false, disableGlyphCaching: false, disableGfx: false, // RDP Device redirection enablePrinting: false, printerName: "", enableDrive: false, driveName: "", drivePath: "", createDrivePath: false, disableDownload: false, disableUpload: false, enableTouch: false, // RDP Session settings clientName: "", console: false, initialProgram: "", serverLayout: "en-us-qwerty", timezone: "", // RDP Gateway settings gatewayHostname: "", gatewayPort: 443, gatewayUsername: "", gatewayPassword: "", gatewayDomain: "", // RDP RemoteApp settings remoteApp: "", remoteAppDir: "", remoteAppArgs: "", // Clipboard settings normalizeClipboard: "preserve", disableCopy: false, disablePaste: false, // VNC specific settings cursor: "remote", swapRedBlue: false, readOnly: false, // Recording settings recordingPath: "", recordingName: "", createRecordingPath: false, recordingExcludeOutput: false, recordingExcludeMouse: false, recordingIncludeKeys: false, // Wake-on-LAN settings wolSendPacket: false, wolMacAddr: "", wolBroadcastAddr: "", wolUdpPort: 9, wolWaitTime: 0, }, }, }); useEffect(() => { if (authTab === "credential") { const currentCredentialId = form.getValues("credentialId"); const overrideUsername = form.getValues("overrideCredentialUsername"); if (currentCredentialId && !overrideUsername) { const selectedCredential = credentials.find( (c) => c.id === currentCredentialId, ); if (selectedCredential) { form.setValue("username", selectedCredential.username); } } } }, [authTab, credentials, form.getValues, form.setValue]); useEffect(() => { if (editingHost) { const cleanedHost = { ...editingHost }; if (cleanedHost.credentialId && cleanedHost.key) { cleanedHost.key = undefined; cleanedHost.keyPassword = undefined; cleanedHost.keyType = undefined; } else if (cleanedHost.credentialId && cleanedHost.password) { cleanedHost.password = undefined; } else if (cleanedHost.key && cleanedHost.password) { cleanedHost.password = undefined; } const defaultAuthType = cleanedHost.credentialId ? "credential" : cleanedHost.key ? "key" : cleanedHost.password ? "password" : "none"; setAuthTab(defaultAuthType); let parsedStatsConfig = DEFAULT_STATS_CONFIG; try { if (cleanedHost.statsConfig) { parsedStatsConfig = typeof cleanedHost.statsConfig === "string" ? JSON.parse(cleanedHost.statsConfig) : cleanedHost.statsConfig; } } catch (error) { console.error("Failed to parse statsConfig:", error); } parsedStatsConfig = { ...DEFAULT_STATS_CONFIG, ...parsedStatsConfig }; let parsedDockerConfig = { connectionType: "socket" as const, socketPath: "/var/run/docker.sock", host: "", port: 2375, tlsVerify: true, tlsCaCert: "", tlsCert: "", tlsKey: "", }; try { if (cleanedHost.dockerConfig) { const parsed = typeof cleanedHost.dockerConfig === "string" ? JSON.parse(cleanedHost.dockerConfig) : cleanedHost.dockerConfig; parsedDockerConfig = { ...parsedDockerConfig, ...parsed }; } } catch (error) { console.error("Failed to parse dockerConfig:", error); } // Parse guacamoleConfig if it exists - merge with defaults const defaultGuacamoleConfig = { colorDepth: undefined, width: undefined, height: undefined, dpi: 96, resizeMethod: "display-update", forceLossless: false, disableAudio: false, enableAudioInput: false, enableWallpaper: false, enableTheming: false, enableFontSmoothing: true, enableFullWindowDrag: false, enableDesktopComposition: false, enableMenuAnimations: false, disableBitmapCaching: false, disableOffscreenCaching: false, disableGlyphCaching: false, disableGfx: false, enablePrinting: false, printerName: "", enableDrive: false, driveName: "", drivePath: "", createDrivePath: false, disableDownload: false, disableUpload: false, enableTouch: false, clientName: "", console: false, initialProgram: "", serverLayout: "en-us-qwerty", timezone: "", gatewayHostname: "", gatewayPort: 443, gatewayUsername: "", gatewayPassword: "", gatewayDomain: "", remoteApp: "", remoteAppDir: "", remoteAppArgs: "", normalizeClipboard: "preserve", disableCopy: false, disablePaste: false, cursor: "remote", swapRedBlue: false, readOnly: false, recordingPath: "", recordingName: "", createRecordingPath: false, recordingExcludeOutput: false, recordingExcludeMouse: false, recordingIncludeKeys: false, wolSendPacket: false, wolMacAddr: "", wolBroadcastAddr: "", wolUdpPort: 9, wolWaitTime: 0, }; let parsedGuacamoleConfig = { ...defaultGuacamoleConfig }; try { if (cleanedHost.guacamoleConfig) { console.log("[HostManagerEditor] Loading host guacamoleConfig:", cleanedHost.guacamoleConfig); const parsed = typeof cleanedHost.guacamoleConfig === "string" ? JSON.parse(cleanedHost.guacamoleConfig) : cleanedHost.guacamoleConfig; parsedGuacamoleConfig = { ...defaultGuacamoleConfig, ...parsed }; console.log("[HostManagerEditor] Merged guacamoleConfig:", parsedGuacamoleConfig); } } catch (error) { console.error("Failed to parse guacamoleConfig:", error); } const formData = { connectionType: (cleanedHost.connectionType || "ssh") as "ssh" | "rdp" | "vnc" | "telnet", name: cleanedHost.name || "", ip: cleanedHost.ip || "", port: cleanedHost.port || 22, username: cleanedHost.username || "", folder: cleanedHost.folder || "", tags: Array.isArray(cleanedHost.tags) ? cleanedHost.tags : [], pin: Boolean(cleanedHost.pin), authType: defaultAuthType as "password" | "key" | "credential" | "none", credentialId: null, overrideCredentialUsername: Boolean( cleanedHost.overrideCredentialUsername, ), password: "", key: null, keyPassword: "", keyType: "auto" as const, enableTerminal: Boolean(cleanedHost.enableTerminal), enableTunnel: Boolean(cleanedHost.enableTunnel), enableFileManager: Boolean(cleanedHost.enableFileManager), defaultPath: cleanedHost.defaultPath || "/", tunnelConnections: Array.isArray(cleanedHost.tunnelConnections) ? cleanedHost.tunnelConnections : [], jumpHosts: Array.isArray(cleanedHost.jumpHosts) ? cleanedHost.jumpHosts : [], quickActions: Array.isArray(cleanedHost.quickActions) ? cleanedHost.quickActions : [], statsConfig: parsedStatsConfig, terminalConfig: { ...DEFAULT_TERMINAL_CONFIG, ...(cleanedHost.terminalConfig || {}), environmentVariables: Array.isArray( cleanedHost.terminalConfig?.environmentVariables, ) ? cleanedHost.terminalConfig.environmentVariables : [], }, forceKeyboardInteractive: Boolean(cleanedHost.forceKeyboardInteractive), enableDocker: Boolean(cleanedHost.enableDocker), dockerConfig: parsedDockerConfig, // RDP/VNC specific fields domain: cleanedHost.domain || "", security: cleanedHost.security || "", ignoreCert: Boolean(cleanedHost.ignoreCert), // Guacamole config for RDP/VNC guacamoleConfig: parsedGuacamoleConfig, }; if (defaultAuthType === "password") { formData.password = cleanedHost.password || ""; } else if (defaultAuthType === "key") { formData.key = editingHost.id ? "existing_key" : editingHost.key; formData.keyPassword = cleanedHost.keyPassword || ""; formData.keyType = (cleanedHost.keyType as | "auto" | "ssh-rsa" | "ssh-ed25519" | "ecdsa-sha2-nistp256" | "ecdsa-sha2-nistp384" | "ecdsa-sha2-nistp521" | "ssh-dss" | "ssh-rsa-sha2-256" | "ssh-rsa-sha2-512") || "auto"; } else if (defaultAuthType === "credential") { formData.credentialId = cleanedHost.credentialId || "existing_credential"; } form.reset(formData); } else { setAuthTab("password"); const defaultFormData = { connectionType: "ssh" as const, name: "", ip: "", port: 22, username: "", folder: "", tags: [], pin: false, authType: "password" as const, credentialId: null, overrideCredentialUsername: false, password: "", key: null, keyPassword: "", keyType: "auto" as const, enableTerminal: true, enableTunnel: true, enableFileManager: true, defaultPath: "/", tunnelConnections: [], jumpHosts: [], quickActions: [], statsConfig: DEFAULT_STATS_CONFIG, terminalConfig: DEFAULT_TERMINAL_CONFIG, forceKeyboardInteractive: false, enableDocker: false, dockerConfig: { connectionType: "socket" as const, socketPath: "/var/run/docker.sock", host: "", port: 2375, tlsVerify: true, tlsCaCert: "", tlsCert: "", tlsKey: "", }, // RDP/VNC specific defaults domain: "", security: "", ignoreCert: false, }; form.reset(defaultFormData); } }, [editingHost?.id]); useEffect(() => { const focusTimer = setTimeout(() => { if (ipInputRef.current) { ipInputRef.current.focus(); } }, 300); return () => clearTimeout(focusTimer); }, [editingHost]); const onSubmit = async (data: FormData) => { try { isSubmittingRef.current = true; setFormError(null); // Debug: log form data being submitted console.log("[HostManagerEditor] Form data on submit:", data); console.log("[HostManagerEditor] data.guacamoleConfig:", data.guacamoleConfig); console.log("[HostManagerEditor] data.guacamoleConfig.enableWallpaper:", data.guacamoleConfig?.enableWallpaper); console.log("[HostManagerEditor] form.getValues('guacamoleConfig'):", form.getValues("guacamoleConfig")); if (!data.name || data.name.trim() === "") { data.name = `${data.username}@${data.ip}`; } if (data.statsConfig) { const statusInterval = data.statsConfig.statusCheckInterval || 30; const metricsInterval = data.statsConfig.metricsInterval || 30; if (statusInterval < 5 || statusInterval > 3600) { toast.error(t("hosts.intervalValidation")); setActiveTab("statistics"); setFormError(t("hosts.intervalValidation")); isSubmittingRef.current = false; return; } if (metricsInterval < 5 || metricsInterval > 3600) { toast.error(t("hosts.intervalValidation")); setActiveTab("statistics"); setFormError(t("hosts.intervalValidation")); isSubmittingRef.current = false; return; } } const submitData: Record = { connectionType: data.connectionType || "ssh", name: data.name, ip: data.ip, port: data.port, username: data.username, folder: data.folder || "", tags: data.tags || [], pin: Boolean(data.pin), authType: data.authType, overrideCredentialUsername: Boolean(data.overrideCredentialUsername), enableTerminal: Boolean(data.enableTerminal), enableDocker: Boolean(data.enableDocker), dockerConfig: data.dockerConfig || null, enableTunnel: Boolean(data.enableTunnel), enableFileManager: Boolean(data.enableFileManager), defaultPath: data.defaultPath || "/", tunnelConnections: data.tunnelConnections || [], jumpHosts: data.jumpHosts || [], quickActions: data.quickActions || [], statsConfig: data.statsConfig || DEFAULT_STATS_CONFIG, terminalConfig: data.terminalConfig || DEFAULT_TERMINAL_CONFIG, forceKeyboardInteractive: Boolean(data.forceKeyboardInteractive), // RDP/VNC specific fields domain: data.domain || null, security: data.security || null, ignoreCert: Boolean(data.ignoreCert), // Guacamole configuration for RDP/VNC guacamoleConfig: data.guacamoleConfig || null, }; // Debug: log what we're submitting console.log("[HostManagerEditor] submitData.guacamoleConfig:", submitData.guacamoleConfig); submitData.credentialId = null; submitData.password = null; submitData.key = null; submitData.keyPassword = null; submitData.keyType = null; if (data.authType === "credential") { if ( data.credentialId === "existing_credential" && editingHost && editingHost.id ) { delete submitData.credentialId; } else { submitData.credentialId = data.credentialId; } } else if (data.authType === "password") { submitData.password = data.password; } else if (data.authType === "key") { if (data.key instanceof File) { const keyContent = await data.key.text(); submitData.key = keyContent; } else if (data.key === "existing_key") { delete submitData.key; } else { submitData.key = data.key; } submitData.keyPassword = data.keyPassword; submitData.keyType = data.keyType; } let savedHost; if (editingHost && editingHost.id) { savedHost = await updateSSHHost(editingHost.id, submitData); toast.success(t("hosts.hostUpdatedSuccessfully", { name: data.name })); } else { savedHost = await createSSHHost(submitData); toast.success(t("hosts.hostAddedSuccessfully", { name: data.name })); } if (savedHost && savedHost.id && data.tunnelConnections) { const hasAutoStartTunnels = data.tunnelConnections.some( (tunnel) => tunnel.autoStart, ); if (hasAutoStartTunnels) { try { await enableAutoStart(savedHost.id); } catch (error) { console.warn( `Failed to enable AutoStart plaintext cache for SSH host ${savedHost.id}:`, error, ); toast.warning( t("hosts.autoStartEnableFailed", { name: data.name }), ); } } else { try { await disableAutoStart(savedHost.id); } catch (error) { console.warn( `Failed to disable AutoStart plaintext cache for SSH host ${savedHost.id}:`, error, ); } } } if (onFormSubmit) { onFormSubmit(savedHost); } window.dispatchEvent(new CustomEvent("ssh-hosts:changed")); if (savedHost?.id) { const { notifyHostCreatedOrUpdated } = await import("@/ui/main-axios.ts"); notifyHostCreatedOrUpdated(savedHost.id); } } catch (error) { toast.error(t("hosts.failedToSaveHost")); console.error("Failed to save host:", error); } finally { isSubmittingRef.current = false; } }; const handleFormError = () => { const errors = form.formState.errors; if ( errors.ip || errors.port || errors.username || errors.name || errors.folder || errors.tags || errors.pin || errors.password || errors.key || errors.keyPassword || errors.keyType || errors.credentialId || errors.forceKeyboardInteractive || errors.jumpHosts ) { setActiveTab("general"); } else if (errors.enableTerminal || errors.terminalConfig) { setActiveTab("terminal"); } else if (errors.enableDocker || errors.dockerConfig) { setActiveTab("docker"); } else if (errors.enableTunnel || errors.tunnelConnections) { setActiveTab("tunnel"); } else if (errors.enableFileManager || errors.defaultPath) { setActiveTab("file_manager"); } else if (errors.statsConfig) { setActiveTab("statistics"); } }; const [tagInput, setTagInput] = useState(""); const [folderDropdownOpen, setFolderDropdownOpen] = useState(false); const folderInputRef = useRef(null); const folderDropdownRef = useRef(null); const folderValue = form.watch("folder"); const filteredFolders = React.useMemo(() => { if (!folderValue) return folders; return folders.filter((f) => f.toLowerCase().includes(folderValue.toLowerCase()), ); }, [folderValue, folders]); const handleFolderClick = (folder: string) => { form.setValue("folder", folder); setFolderDropdownOpen(false); }; useEffect(() => { function handleClickOutside(event: MouseEvent) { if ( folderDropdownRef.current && !folderDropdownRef.current.contains(event.target as Node) && folderInputRef.current && !folderInputRef.current.contains(event.target as Node) ) { setFolderDropdownOpen(false); } } if (folderDropdownOpen) { document.addEventListener("mousedown", handleClickOutside); } else { document.removeEventListener("mousedown", handleClickOutside); } return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [folderDropdownOpen]); const keyTypeOptions = [ { value: "auto", label: t("hosts.autoDetect") }, { value: "ssh-rsa", label: t("hosts.rsa") }, { value: "ssh-ed25519", label: t("hosts.ed25519") }, { value: "ecdsa-sha2-nistp256", label: t("hosts.ecdsaNistP256") }, { value: "ecdsa-sha2-nistp384", label: t("hosts.ecdsaNistP384") }, { value: "ecdsa-sha2-nistp521", label: t("hosts.ecdsaNistP521") }, { value: "ssh-dss", label: t("hosts.dsa") }, { value: "ssh-rsa-sha2-256", label: t("hosts.rsaSha2256") }, { value: "ssh-rsa-sha2-512", label: t("hosts.rsaSha2512") }, ]; const [keyTypeDropdownOpen, setKeyTypeDropdownOpen] = useState(false); const keyTypeButtonRef = useRef(null); const keyTypeDropdownRef = useRef(null); useEffect(() => { function onClickOutside(event: MouseEvent) { if ( keyTypeDropdownOpen && keyTypeDropdownRef.current && !keyTypeDropdownRef.current.contains(event.target as Node) && keyTypeButtonRef.current && !keyTypeButtonRef.current.contains(event.target as Node) ) { setKeyTypeDropdownOpen(false); } } document.addEventListener("mousedown", onClickOutside); return () => document.removeEventListener("mousedown", onClickOutside); }, [keyTypeDropdownOpen]); const [sshConfigDropdownOpen, setSshConfigDropdownOpen] = useState<{ [key: number]: boolean; }>({}); const sshConfigInputRefs = useRef<{ [key: number]: HTMLInputElement | null }>( {}, ); const sshConfigDropdownRefs = useRef<{ [key: number]: HTMLDivElement | null; }>({}); const getFilteredSshConfigs = (index: number) => { const value = form.watch(`tunnelConnections.${index}.endpointHost`); const currentHostId = editingHost?.id; let filtered = sshConfigurations; if (currentHostId) { const currentHostName = hosts.find((h) => h.id === currentHostId)?.name; if (currentHostName) { filtered = sshConfigurations.filter( (config) => config !== currentHostName, ); } } else { const currentHostName = form.watch("name") || `${form.watch("username")}@${form.watch("ip")}`; filtered = sshConfigurations.filter( (config) => config !== currentHostName, ); } if (value) { filtered = filtered.filter((config) => config.toLowerCase().includes(value.toLowerCase()), ); } return filtered; }; const handleSshConfigClick = (config: string, index: number) => { form.setValue(`tunnelConnections.${index}.endpointHost`, config); setSshConfigDropdownOpen((prev) => ({ ...prev, [index]: false })); }; useEffect(() => { function handleSshConfigClickOutside(event: MouseEvent) { const openDropdowns = Object.keys(sshConfigDropdownOpen).filter( (key) => sshConfigDropdownOpen[parseInt(key)], ); openDropdowns.forEach((indexStr: string) => { const index = parseInt(indexStr); if ( sshConfigDropdownRefs.current[index] && !sshConfigDropdownRefs.current[index]?.contains( event.target as Node, ) && sshConfigInputRefs.current[index] && !sshConfigInputRefs.current[index]?.contains(event.target as Node) ) { setSshConfigDropdownOpen((prev) => ({ ...prev, [index]: false })); } }); } const hasOpenDropdowns = Object.values(sshConfigDropdownOpen).some( (open) => open, ); if (hasOpenDropdowns) { document.addEventListener("mousedown", handleSshConfigClickOutside); } else { document.removeEventListener("mousedown", handleSshConfigClickOutside); } return () => { document.removeEventListener("mousedown", handleSshConfigClickOutside); }; }, [sshConfigDropdownOpen]); return (
{formError && ( {formError} )} {/* Only show tabs if there's more than just the General tab (SSH has extra tabs) */} {form.watch("connectionType") === "ssh" && ( {t("hosts.general")} {t("hosts.terminal")} Docker {t("hosts.tunnel")} {t("hosts.fileManager")} {t("hosts.statistics")} )} {/* RDP tabs */} {form.watch("connectionType") === "rdp" && ( {t("hosts.general")} {t("hosts.display", "Display")} {t("hosts.audio", "Audio")} {t("hosts.performance", "Performance")} )} {/* VNC tabs */} {form.watch("connectionType") === "vnc" && ( {t("hosts.general")} {t("hosts.display", "Display")} {t("hosts.audio", "Audio")} )} {t("hosts.connectionType", "Connection Type")}
(
{[ { value: "ssh", label: "SSH" }, { value: "rdp", label: "RDP" }, { value: "vnc", label: "VNC" }, { value: "telnet", label: "Telnet" }, ].map((option) => ( ))}
)} />
{t("hosts.connectionDetails")}
( {t("hosts.ipAddress")} { field.ref(e); ipInputRef.current = e; }} /> )} /> ( {t("hosts.port")} )} /> { const isCredentialAuth = authTab === "credential"; const hasCredential = !!form.watch("credentialId"); const overrideEnabled = !!form.watch( "overrideCredentialUsername", ); const shouldDisable = isCredentialAuth && hasCredential && !overrideEnabled; return ( {t("hosts.username")} ); }} />
{/* RDP-specific fields */} {form.watch("connectionType") === "rdp" && ( <> {t("hosts.rdpSettings", "RDP Settings")}
( {t("hosts.domain", "Domain")} )} /> ( {t("hosts.security", "Security")} )} /> (
{t("hosts.ignoreCert", "Ignore Certificate")}
)} />
)} {t("hosts.organization")}
( {t("hosts.name")} )} /> ( {t("hosts.folder")} setFolderDropdownOpen(true)} onChange={(e) => { field.onChange(e); setFolderDropdownOpen(true); }} /> {folderDropdownOpen && filteredFolders.length > 0 && (
{filteredFolders.map((folder) => ( ))}
)}
)} /> ( {t("hosts.tags")}
{field.value.map((tag: string, idx: number) => ( {tag} ))} setTagInput(e.target.value)} onKeyDown={(e) => { if (e.key === " " && tagInput.trim() !== "") { e.preventDefault(); if ( !field.value.includes(tagInput.trim()) ) { field.onChange([ ...field.value, tagInput.trim(), ]); } setTagInput(""); } else if ( e.key === "Backspace" && tagInput === "" && field.value.length > 0 ) { field.onChange(field.value.slice(0, -1)); } }} placeholder={t("hosts.addTagsSpaceToAdd")} />
)} /> ( {t("hosts.pin")} )} />
{/* Authentication section - only for SSH and Telnet */} {(form.watch("connectionType") === "ssh" || form.watch("connectionType") === "telnet") && ( <> {t("hosts.authentication")} { const newAuthType = value as | "password" | "key" | "credential" | "none"; setAuthTab(newAuthType); form.setValue("authType", newAuthType); }} className="flex-1 flex flex-col h-full min-h-0" > {t("hosts.password")} {form.watch("connectionType") === "ssh" && ( {t("hosts.key")} )} {t("hosts.credential")} {t("hosts.none")} ( {t("hosts.password")} )} /> { setKeyInputMethod(value as "upload" | "paste"); if (value === "upload") { form.setValue("key", null); } else { form.setValue("key", ""); } }} className="w-full" > {t("hosts.uploadFile")} {t("hosts.pasteKey")} ( {t("hosts.sshPrivateKey")}
{ const file = e.target.files?.[0]; field.onChange(file || null); }} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
)} />
( {t("hosts.sshPrivateKey")} field.onChange(value)} placeholder={t( "placeholders.pastePrivateKey", )} theme={oneDark} className="border border-input rounded-md" minHeight="120px" basicSetup={{ lineNumbers: true, foldGutter: false, dropCursor: false, allowMultipleSelections: false, highlightSelectionMatches: false, searchKeymap: false, scrollPastEnd: false, }} extensions={[ EditorView.theme({ ".cm-scroller": { overflow: "auto", }, }), ]} /> )} />
( {t("hosts.keyPassword")} )} /> ( {t("hosts.keyType")}
{keyTypeDropdownOpen && (
{keyTypeOptions.map((opt) => ( ))}
)}
)} />
( { if ( credential && !form.getValues( "overrideCredentialUsername", ) ) { form.setValue( "username", credential.username, ); } }} /> {t("hosts.credentialDescription")} )} /> {form.watch("credentialId") && ( (
{t("hosts.overrideCredentialUsername")} {t("hosts.overrideCredentialUsernameDesc")}
)} /> )}
{t("hosts.noneAuthTitle")}
{t("hosts.noneAuthDescription")}
{t("hosts.noneAuthDetails")}
{t("hosts.advancedAuthSettings")} (
{t("hosts.forceKeyboardInteractive")} {t("hosts.forceKeyboardInteractiveDesc")}
)} />
{t("hosts.jumpHosts")} {t("hosts.jumpHostsDescription")} ( {t("hosts.jumpHostChain")}
{field.value.map((jumpHost, index) => ( { const newJumpHosts = [...field.value]; newJumpHosts[index] = { hostId }; field.onChange(newJumpHosts); }} onRemove={() => { const newJumpHosts = field.value.filter( (_, i) => i !== index, ); field.onChange(newJumpHosts); }} t={t} /> ))}
{t("hosts.jumpHostsOrder")}
)} />
)} {/* RDP/VNC password authentication - simpler than SSH */} {(form.watch("connectionType") === "rdp" || form.watch("connectionType") === "vnc") && ( <> {t("hosts.authentication")}
( {t("hosts.password")} )} />
)}
( {t("hosts.enableTerminal")} {t("hosts.enableTerminalDesc")} )} /> {t("hosts.terminalCustomizationNotice")}

{t("hosts.terminalCustomization")}

{t("hosts.appearance")}
( {t("hosts.theme")} {t("hosts.chooseColorTheme")} )} /> ( {t("hosts.fontFamily")} {t("hosts.selectFontDesc")} )} /> ( {t("hosts.fontSizeValue", { value: field.value, })} field.onChange(value) } /> {t("hosts.adjustFontSize")} )} /> ( {t("hosts.letterSpacingValue", { value: field.value, })} field.onChange(value) } /> {t("hosts.adjustLetterSpacing")} )} /> ( {t("hosts.lineHeightValue", { value: field.value, })} field.onChange(value) } /> {t("hosts.adjustLineHeight")} )} /> ( {t("hosts.cursorStyle")} {t("hosts.chooseCursorAppearance")} )} /> (
{t("hosts.cursorBlink")} {t("hosts.enableCursorBlink")}
)} />
{t("hosts.behavior")} ( {t("hosts.scrollbackBufferValue", { value: field.value, })} field.onChange(value) } /> {t("hosts.scrollbackBufferDesc")} )} /> ( {t("hosts.bellStyle")} {t("hosts.bellStyleDesc")} )} /> (
{t("hosts.rightClickSelectsWord")} {t("hosts.rightClickSelectsWordDesc")}
)} /> ( {t("hosts.fastScrollModifier")} {t("hosts.fastScrollModifierDesc")} )} /> ( {t("hosts.fastScrollSensitivityValue", { value: field.value, })} field.onChange(value) } /> {t("hosts.fastScrollSensitivityDesc")} )} /> ( {t("hosts.minimumContrastRatioValue", { value: field.value, })} field.onChange(value) } /> {t("hosts.minimumContrastRatioDesc")} )} />
{t("hosts.advanced")} (
{t("hosts.sshAgentForwarding")} {t("hosts.sshAgentForwardingDesc")}
)} /> ( {t("hosts.backspaceMode")} {t("hosts.backspaceModeDesc")} )} /> { const [open, setOpen] = React.useState(false); const selectedSnippet = snippets.find( (s) => s.id === field.value, ); return ( {t("hosts.startupSnippet")} {t("hosts.noSnippetFound")} { field.onChange(null); setOpen(false); }} > {t("hosts.snippetNone")} {snippets.map((snippet) => ( { field.onChange(snippet.id); setOpen(false); }} >
{snippet.name} {snippet.content}
))}
Execute a snippet when the terminal connects
); }} /> (
Auto-MOSH Automatically run MOSH command on connect
)} /> {form.watch("terminalConfig.autoMosh") && ( ( MOSH Command The MOSH command to execute )} /> )} (
{t("hosts.sudoPasswordAutoFill")} {t("hosts.sudoPasswordAutoFillDesc")}
)} />
Set custom environment variables for the terminal session {form .watch("terminalConfig.environmentVariables") ?.map((_, index) => (
( )} /> ( )} />
))}
( Enable Docker Enable Docker integration for this host )} /> {form.watch("enableDocker") && ( <> Docker Configuration
Configure connection to Docker daemon on this host. You can connect via Unix socket, TCP, or secure TLS connection.
( Connection Type Choose how to connect to the Docker daemon )} /> {form.watch("dockerConfig.connectionType") === "socket" && ( ( Socket Path Path to the Docker Unix socket (default: /var/run/docker.sock) )} /> )} {(form.watch("dockerConfig.connectionType") === "tcp" || form.watch("dockerConfig.connectionType") === "tls") && ( <>
( Docker Host )} /> ( Port )} />
)} {form.watch("dockerConfig.connectionType") === "tls" && ( TLS Configuration (
Verify TLS Verify the Docker daemon's certificate
)} /> ( CA Certificate Certificate Authority certificate (PEM format) )} /> ( Client Certificate Client certificate (PEM format) )} /> ( Client Key Client private key (PEM format) )} />
)} )}
( {t("hosts.enableTunnel")} {t("hosts.enableTunnelDesc")} )} /> {form.watch("enableTunnel") && ( <> {t("hosts.sshpassRequired")}
{t("hosts.sshpassRequiredDesc")}{" "} sudo apt install sshpass {" "} {t("hosts.debianUbuntuEquivalent")}
{t("hosts.otherInstallMethods")}
• {t("hosts.centosRhelFedora")}{" "} sudo yum install sshpass {" "} {t("hosts.or")}{" "} sudo dnf install sshpass
• {t("hosts.macos")}{" "} brew install hudochenkov/sshpass/sshpass
• {t("hosts.windows")}
{t("hosts.sshServerConfigRequired")}
{t("hosts.sshServerConfigDesc")}
•{" "} GatewayPorts yes {" "} {t("hosts.gatewayPortsYes")}
•{" "} AllowTcpForwarding yes {" "} {t("hosts.allowTcpForwardingYes")}
•{" "} PermitRootLogin yes {" "} {t("hosts.permitRootLoginYes")}
{t("hosts.editSshConfig")}
( {t("hosts.tunnelConnections")}
{field.value.map((connection, index) => (

{t("hosts.connection")} {index + 1}

( {t("hosts.sourcePort")} {t("hosts.sourcePortDesc")} )} /> ( {t("hosts.endpointPort")} )} /> ( {t("hosts.endpointSshConfig")} { sshConfigInputRefs.current[ index ] = el; }} placeholder={t( "placeholders.sshConfig", )} className="min-h-[40px]" autoComplete="off" value={endpointHostField.value} onFocus={() => setSshConfigDropdownOpen( (prev) => ({ ...prev, [index]: true, }), ) } onChange={(e) => { endpointHostField.onChange(e); setSshConfigDropdownOpen( (prev) => ({ ...prev, [index]: true, }), ); }} /> {sshConfigDropdownOpen[index] && getFilteredSshConfigs(index) .length > 0 && (
{ sshConfigDropdownRefs.current[ index ] = el; }} className="absolute top-full left-0 z-50 mt-1 w-full bg-dark-bg border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1" >
{getFilteredSshConfigs( index, ).map((config) => ( ))}
)}
)} />

{t("hosts.tunnelForwardDescription", { sourcePort: form.watch( `tunnelConnections.${index}.sourcePort`, ) || "22", endpointPort: form.watch( `tunnelConnections.${index}.endpointPort`, ) || "224", })}

( {t("hosts.maxRetries")} {t("hosts.maxRetriesDescription")} )} /> ( {t("hosts.retryInterval")} {t( "hosts.retryIntervalDescription", )} )} /> ( {t("hosts.autoStartContainer")} {t("hosts.autoStartDesc")} )} />
))}
)} /> )}
( {t("hosts.enableFileManager")} {t("hosts.enableFileManagerDesc")} )} /> {form.watch("enableFileManager") && (
( {t("hosts.defaultPath")} {t("hosts.defaultPathDesc")} )} />
)}
(
{t("hosts.statusCheckEnabled")} {t("hosts.statusCheckEnabledDesc")}
)} /> {form.watch("statsConfig.statusCheckEnabled") && ( { const displayValue = statusIntervalUnit === "minutes" ? Math.round((field.value || 30) / 60) : field.value || 30; const handleIntervalChange = (value: string) => { const numValue = parseInt(value) || 0; const seconds = statusIntervalUnit === "minutes" ? numValue * 60 : numValue; field.onChange(seconds); }; return ( {t("hosts.statusCheckInterval")}
handleIntervalChange(e.target.value) } className="flex-1" />
{t("hosts.statusCheckIntervalDesc")}
); }} /> )}
(
{t("hosts.metricsEnabled")} {t("hosts.metricsEnabledDesc")}
)} /> {form.watch("statsConfig.metricsEnabled") && ( { const displayValue = metricsIntervalUnit === "minutes" ? Math.round((field.value || 30) / 60) : field.value || 30; const handleIntervalChange = (value: string) => { const numValue = parseInt(value) || 0; const seconds = metricsIntervalUnit === "minutes" ? numValue * 60 : numValue; field.onChange(seconds); }; return ( {t("hosts.metricsInterval")}
handleIntervalChange(e.target.value) } className="flex-1" />
{t("hosts.metricsIntervalDesc")}
); }} /> )}
{form.watch("statsConfig.metricsEnabled") && ( <> ( {t("hosts.enabledWidgets")} {t("hosts.enabledWidgetsDesc")}
{( [ "cpu", "memory", "disk", "network", "uptime", "processes", "system", "login_stats", ] as const ).map((widget) => (
{ const currentWidgets = field.value || []; if (checked) { field.onChange([ ...currentWidgets, widget, ]); } else { field.onChange( currentWidgets.filter( (w) => w !== widget, ), ); } }} />
))}
)} /> )}

{t("hosts.quickActions")}

{t("hosts.quickActionsDescription")} ( {t("hosts.quickActionsList")}
{field.value.map((quickAction, index) => ( { const newQuickActions = [...field.value]; newQuickActions[index] = { name, snippetId, }; field.onChange(newQuickActions); }} onRemove={() => { const newQuickActions = field.value.filter( (_, i) => i !== index, ); field.onChange(newQuickActions); }} t={t} /> ))}
{t("hosts.quickActionsOrder")}
)} />
{/* RDP/VNC Display Tab */} {t("hosts.displaySettings", "Display Settings")}
( {t("hosts.width", "Width")} field.onChange(e.target.value ? parseInt(e.target.value) : undefined)} /> {t("hosts.widthDesc", "Display width in pixels (leave empty for auto)")} )} /> ( {t("hosts.height", "Height")} field.onChange(e.target.value ? parseInt(e.target.value) : undefined)} /> {t("hosts.heightDesc", "Display height in pixels (leave empty for auto)")} )} />
( {t("hosts.dpi", "DPI")} field.onChange(e.target.value ? parseInt(e.target.value) : undefined)} /> {t("hosts.dpiDesc", "Display resolution in DPI")} )} /> ( {t("hosts.colorDepth", "Color Depth")} {t("hosts.colorDepthDesc", "Color depth for the remote display")} )} />
{form.watch("connectionType") === "rdp" && ( <> ( {t("hosts.resizeMethod", "Resize Method")} {t("hosts.resizeMethodDesc", "Method to use when resizing the display")} )} /> (
{t("hosts.forceLossless", "Force Lossless")} {t("hosts.forceLosslessDesc", "Force lossless compression (higher bandwidth)")}
)} /> )} {form.watch("connectionType") === "vnc" && ( <> ( {t("hosts.cursor", "Cursor Mode")} {t("hosts.cursorDesc", "How to render the mouse cursor")} )} /> (
{t("hosts.swapRedBlue", "Swap Red/Blue")} {t("hosts.swapRedBlueDesc", "Swap red and blue color components")}
)} /> (
{t("hosts.readOnly", "Read Only")} {t("hosts.readOnlyDesc", "View only mode - no input sent to server")}
)} /> )}
{/* RDP/VNC Audio Tab */} {t("hosts.audioSettings", "Audio Settings")} (
{t("hosts.disableAudio", "Disable Audio")} {t("hosts.disableAudioDesc", "Disable audio playback from the remote session")}
)} /> {form.watch("connectionType") === "rdp" && ( (
{t("hosts.enableAudioInput", "Enable Audio Input")} {t("hosts.enableAudioInputDesc", "Enable microphone input to the remote session")}
)} /> )}
{/* RDP Performance Tab */} {t("hosts.performanceSettings", "Performance Settings")}
(
{t("hosts.enableWallpaper", "Wallpaper")} {t("hosts.enableWallpaperDesc", "Show desktop wallpaper")}
{ console.log("[HostManagerEditor] Wallpaper toggled to:", checked); field.onChange(checked); // Log the full guacamoleConfig after change setTimeout(() => { console.log("[HostManagerEditor] After toggle, guacamoleConfig:", form.getValues("guacamoleConfig")); }, 0); }} />
)} /> (
{t("hosts.enableTheming", "Theming")} {t("hosts.enableThemingDesc", "Enable window theming")}
)} /> (
{t("hosts.enableFontSmoothing", "Font Smoothing")} {t("hosts.enableFontSmoothingDesc", "Enable ClearType font smoothing")}
)} /> (
{t("hosts.enableFullWindowDrag", "Full Window Drag")} {t("hosts.enableFullWindowDragDesc", "Show window contents while dragging")}
)} /> (
{t("hosts.enableDesktopComposition", "Desktop Composition")} {t("hosts.enableDesktopCompositionDesc", "Enable Aero glass effects")}
)} /> (
{t("hosts.enableMenuAnimations", "Menu Animations")} {t("hosts.enableMenuAnimationsDesc", "Enable menu animations")}
)} />
{t("hosts.cachingSettings", "Caching Settings")}
(
{t("hosts.disableBitmapCaching", "Disable Bitmap Caching")} {t("hosts.disableBitmapCachingDesc", "Disable bitmap caching")}
)} /> (
{t("hosts.disableOffscreenCaching", "Disable Offscreen Caching")} {t("hosts.disableOffscreenCachingDesc", "Disable offscreen caching")}
)} /> (
{t("hosts.disableGlyphCaching", "Disable Glyph Caching")} {t("hosts.disableGlyphCachingDesc", "Disable glyph caching")}
)} /> (
{t("hosts.disableGfx", "Disable GFX")} {t("hosts.disableGfxDesc", "Disable graphics pipeline extension")}
)} />
); }