import React, { useState, useEffect, useRef } from "react"; import { useForm, Controller, type Resolver } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog.tsx"; import { Button } from "@/components/ui/button.tsx"; import { Input } from "@/components/ui/input.tsx"; import { PasswordInput } from "@/components/ui/password-input.tsx"; import { Label } from "@/components/ui/label.tsx"; import { Form, FormControl, FormField, FormItem, FormLabel, } from "@/components/ui/form.tsx"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/components/ui/tabs.tsx"; import { Switch } from "@/components/ui/switch.tsx"; import { CredentialSelector } from "@/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx"; import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; import { quickConnect } from "@/ui/main-axios.ts"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import CodeMirror from "@uiw/react-codemirror"; import { oneDark } from "@codemirror/theme-one-dark"; import { githubLight } from "@uiw/codemirror-theme-github"; import { EditorView } from "@codemirror/view"; import { useTheme } from "@/components/theme-provider.tsx"; import type { SSHHost } from "@/types"; interface QuickConnectDialogProps { open: boolean; onOpenChange: (open: boolean) => void; } const keyTypeOptions = [ { value: "auto", label: "Auto Detect" }, { value: "ssh-rsa", label: "RSA" }, { value: "ssh-ed25519", label: "Ed25519" }, { value: "ecdsa-sha2-nistp256", label: "ECDSA NIST P-256" }, { value: "ecdsa-sha2-nistp384", label: "ECDSA NIST P-384" }, { value: "ecdsa-sha2-nistp521", label: "ECDSA NIST P-521" }, { value: "ssh-dss", label: "DSA" }, { value: "ssh-rsa-sha2-256", label: "RSA SHA2-256" }, { value: "ssh-rsa-sha2-512", label: "RSA SHA2-512" }, ]; export function QuickConnectDialog({ open, onOpenChange, }: QuickConnectDialogProps) { const { t } = useTranslation(); const { theme: appTheme } = useTheme(); const { addTab, setCurrentTab } = useTabs(); const [authTab, setAuthTab] = useState<"password" | "key" | "credential">( "password", ); const [keyInputMethod, setKeyInputMethod] = useState<"upload" | "paste">( "upload", ); const [keyTypeDropdownOpen, setKeyTypeDropdownOpen] = useState(false); const keyTypeButtonRef = useRef(null); const keyTypeDropdownRef = useRef(null); const [isConnecting, setIsConnecting] = useState(false); const isDarkMode = appTheme === "dark" || (appTheme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches); const editorTheme = isDarkMode ? oneDark : githubLight; const formSchema = z .object({ ip: z.string().min(1, t("quickConnect.ipAddress")), port: z.coerce.number().min(1).max(65535).default(22), username: z.string().min(1, t("quickConnect.username")), authType: z.enum(["password", "key", "credential"]), password: z.string().optional(), key: z.any().optional(), 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(), credentialId: z.number().optional().nullable(), overrideCredentialUsername: z.boolean().optional(), }) .superRefine((data, ctx) => { if (data.authType === "password") { if ( !data.password || (typeof data.password === "string" && data.password.trim() === "") ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: t("quickConnect.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("quickConnect.keyRequired"), 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) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: t("quickConnect.credentialRequired"), path: ["credentialId"], }); } } }); type FormData = z.infer; const form = useForm({ resolver: zodResolver(formSchema) as unknown as Resolver, mode: "all", defaultValues: { ip: "", port: 22, username: "", authType: "password" as const, password: "", key: null, keyPassword: "", keyType: "auto" as const, credentialId: null, overrideCredentialUsername: false, }, }); useEffect(() => { form.setValue("authType", authTab, { shouldValidate: true }); if (authTab === "password") { form.setValue("key", null, { shouldValidate: true }); form.setValue("keyPassword", "", { shouldValidate: true }); form.setValue("keyType", "auto", { shouldValidate: true }); form.setValue("credentialId", null, { shouldValidate: true }); } else if (authTab === "key") { form.setValue("password", "", { shouldValidate: true }); form.setValue("credentialId", null, { shouldValidate: true }); } else if (authTab === "credential") { form.setValue("password", "", { shouldValidate: true }); form.setValue("key", null, { shouldValidate: true }); form.setValue("keyPassword", "", { shouldValidate: true }); form.setValue("keyType", "auto", { shouldValidate: true }); } }, [authTab, form]); 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 handleConnect = async (connectionType: "terminal" | "file_manager") => { const formData = form.getValues(); const isValid = await form.trigger(); if (!isValid) { return; } setIsConnecting(true); try { let keyContent: string | undefined; if (formData.authType === "key" && formData.key) { if (formData.key instanceof File) { keyContent = await formData.key.text(); } else if (typeof formData.key === "string") { keyContent = formData.key; } } const hostConfig = await quickConnect({ ip: formData.ip, port: formData.port, username: formData.username, authType: formData.authType, password: formData.authType === "password" ? formData.password : undefined, key: formData.authType === "key" ? keyContent : undefined, keyPassword: formData.authType === "key" ? formData.keyPassword : undefined, keyType: formData.authType === "key" ? formData.keyType : undefined, credentialId: formData.authType === "credential" ? formData.credentialId : undefined, overrideCredentialUsername: formData.authType === "credential" ? formData.overrideCredentialUsername : undefined, }); const tabId = addTab({ type: connectionType, title: `${formData.username}@${formData.ip}:${formData.port}`, hostConfig: hostConfig as SSHHost, }); setCurrentTab(tabId); form.reset(); setAuthTab("password"); setKeyInputMethod("upload"); onOpenChange(false); } catch (error) { console.error("Quick connect failed:", error); toast.error( t("quickConnect.connectionFailed") + ": " + (error instanceof Error ? error.message : String(error)), ); } finally { setIsConnecting(false); } }; return ( {t("quickConnect.title")} {t("quickConnect.description")}
( {t("quickConnect.ipAddress")} { field.onChange(e.target.value.trim()); field.onBlur(); }} /> )} /> ( {t("quickConnect.port")} )} />
{ const isCredentialAuth = authTab === "credential"; const hasCredential = !!form.watch("credentialId"); const overrideEnabled = !!form.watch( "overrideCredentialUsername", ); const shouldDisable = isCredentialAuth && hasCredential && !overrideEnabled; return ( {t("quickConnect.username")} { field.onChange(e.target.value.trim()); field.onBlur(); }} /> ); }} />
setAuthTab(value as "password" | "key" | "credential") } className="w-full" > {t("hosts.password")} {t("hosts.key")} {t("quickConnect.credential")} ( {t("quickConnect.password")} )} /> setKeyInputMethod(value as "upload" | "paste") } className="w-full" > {t("quickConnect.uploadFile")} {t("quickConnect.pasteKey")} ( {t("quickConnect.key")}
{ const file = e.target.files?.[0]; field.onChange(file || null); }} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
)} />
( {t("quickConnect.key")} field.onChange(value)} placeholder={t("placeholders.pastePrivateKey")} theme={editorTheme} className="border border-input rounded-md overflow-hidden" minHeight="120px" basicSetup={{ lineNumbers: true, foldGutter: false, dropCursor: false, allowMultipleSelections: false, highlightSelectionMatches: false, }} extensions={[ EditorView.theme({ ".cm-scroller": { overflow: "auto", scrollbarWidth: "thin", scrollbarColor: "var(--scrollbar-thumb) var(--scrollbar-track)", }, }), ]} /> )} />
( {t("quickConnect.keyPassword")} )} /> ( {t("quickConnect.keyType")}
{keyTypeDropdownOpen && (
{keyTypeOptions.map((opt) => ( ))}
)}
)} />
( { if ( credential && !form.getValues("overrideCredentialUsername") ) { form.setValue("username", credential.username); } }} /> )} /> {form.watch("credentialId") && ( (
{t("quickConnect.overrideUsername")}

{t("quickConnect.overrideUsernameDesc")}

)} /> )}
); }