From 94d87ff406e28b6ba8606f015b8f3eec5b2706f1 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Fri, 26 Dec 2025 01:21:21 -0600 Subject: [PATCH] fix: add unnversiioned files --- .../tabs/CredentialAuthenticationTab.tsx | 540 ++++++++++++ .../credentials/tabs/CredentialGeneralTab.tsx | 237 ++++++ .../hosts/tabs/HostAuthenticationSection.tsx | 349 ++++++++ .../host-manager/hosts/tabs/HostDockerTab.tsx | 29 + .../hosts/tabs/HostFileManagerTab.tsx | 61 ++ .../hosts/tabs/HostGeneralTab.tsx | 785 ++++++++++++++++++ .../hosts/tabs/HostStatisticsTab.tsx | 284 +++++++ .../hosts/tabs/HostTerminalTab.tsx | 646 ++++++++++++++ .../host-manager/hosts/tabs/HostTunnelTab.tsx | 441 ++++++++++ .../hosts/tabs/shared/JumpHostItem.tsx | 104 +++ .../hosts/tabs/shared/QuickActionItem.tsx | 111 +++ .../hosts/tabs/shared/tab-types.ts | 175 ++++ 12 files changed, 3762 insertions(+) create mode 100644 src/ui/desktop/apps/host-manager/credentials/tabs/CredentialAuthenticationTab.tsx create mode 100644 src/ui/desktop/apps/host-manager/credentials/tabs/CredentialGeneralTab.tsx create mode 100644 src/ui/desktop/apps/host-manager/hosts/tabs/HostAuthenticationSection.tsx create mode 100644 src/ui/desktop/apps/host-manager/hosts/tabs/HostDockerTab.tsx create mode 100644 src/ui/desktop/apps/host-manager/hosts/tabs/HostFileManagerTab.tsx create mode 100644 src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx create mode 100644 src/ui/desktop/apps/host-manager/hosts/tabs/HostStatisticsTab.tsx create mode 100644 src/ui/desktop/apps/host-manager/hosts/tabs/HostTerminalTab.tsx create mode 100644 src/ui/desktop/apps/host-manager/hosts/tabs/HostTunnelTab.tsx create mode 100644 src/ui/desktop/apps/host-manager/hosts/tabs/shared/JumpHostItem.tsx create mode 100644 src/ui/desktop/apps/host-manager/hosts/tabs/shared/QuickActionItem.tsx create mode 100644 src/ui/desktop/apps/host-manager/hosts/tabs/shared/tab-types.ts diff --git a/src/ui/desktop/apps/host-manager/credentials/tabs/CredentialAuthenticationTab.tsx b/src/ui/desktop/apps/host-manager/credentials/tabs/CredentialAuthenticationTab.tsx new file mode 100644 index 00000000..d887fb01 --- /dev/null +++ b/src/ui/desktop/apps/host-manager/credentials/tabs/CredentialAuthenticationTab.tsx @@ -0,0 +1,540 @@ +import React from "react"; +import { Controller } from "react-hook-form"; +import { + FormControl, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/form.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import { PasswordInput } from "@/components/ui/password-input.tsx"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs.tsx"; +import CodeMirror from "@uiw/react-codemirror"; +import { EditorView } from "@codemirror/view"; +import { toast } from "sonner"; +import type { Control, UseFormWatch, UseFormSetValue } from "react-hook-form"; + +interface CredentialAuthenticationTabProps { + control: Control; + watch: UseFormWatch; + setValue: UseFormSetValue; + authTab: "password" | "key"; + setAuthTab: (tab: "password" | "key") => void; + editorTheme: any; + detectedKeyType: string | null; + keyDetectionLoading: boolean; + detectedPublicKeyType: string | null; + publicKeyDetectionLoading: boolean; + debouncedKeyDetection: (keyValue: string, keyPassword?: string) => void; + debouncedPublicKeyDetection: (publicKeyValue: string) => void; + generateKeyPair: ( + type: string, + bits?: number, + passphrase?: string, + ) => Promise<{ + success: boolean; + privateKey: string; + publicKey: string; + error?: string; + }>; + generatePublicKeyFromPrivate: ( + privateKey: string, + passphrase?: string, + ) => Promise<{ success: boolean; publicKey?: string; error?: string }>; + getFriendlyKeyTypeName: (keyType: string) => string; + t: (key: string, params?: any) => string; +} + +export function CredentialAuthenticationTab({ + control, + watch, + setValue, + authTab, + setAuthTab, + editorTheme, + detectedKeyType, + keyDetectionLoading, + detectedPublicKeyType, + publicKeyDetectionLoading, + debouncedKeyDetection, + debouncedPublicKeyDetection, + generateKeyPair, + generatePublicKeyFromPrivate, + getFriendlyKeyTypeName, + t, +}: CredentialAuthenticationTabProps) { + return ( + <> + + {t("credentials.authentication")} + + { + const newAuthType = value as "password" | "key"; + setAuthTab(newAuthType); + setValue("authType", newAuthType); + + setValue("password", ""); + setValue("key", null); + setValue("keyPassword", ""); + setValue("keyType", "auto"); + }} + className="flex-1 flex flex-col h-full min-h-0" + > + + + {t("credentials.password")} + + + {t("credentials.key")} + + + + ( + + {t("credentials.password")} + + + + + )} + /> + + +
+
+ + {t("credentials.generateKeyPair")} + + +
+
+ {t("credentials.generateKeyPairDescription")} +
+
+ +
+ + + +
+
+
+ ( + + + {t("credentials.sshPrivateKey")} + +
+
+ { + const file = e.target.files?.[0]; + if (file) { + try { + const fileContent = await file.text(); + field.onChange(fileContent); + debouncedKeyDetection( + fileContent, + watch("keyPassword"), + ); + } catch (error) { + console.error( + "Failed to read uploaded file:", + error, + ); + } + } + }} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + /> + +
+
+ + { + field.onChange(value); + debouncedKeyDetection(value, watch("keyPassword")); + }} + 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, + searchKeymap: false, + scrollPastEnd: false, + }} + extensions={[ + EditorView.theme({ + ".cm-scroller": { + overflow: "auto", + scrollbarWidth: "thin", + scrollbarColor: + "var(--scrollbar-thumb) var(--scrollbar-track)", + }, + }), + ]} + /> + + {detectedKeyType && ( +
+ + {t("credentials.detectedKeyType")}:{" "} + + + {getFriendlyKeyTypeName(detectedKeyType)} + + {keyDetectionLoading && ( + + ({t("credentials.detectingKeyType")}) + + )} +
+ )} +
+ )} + /> + ( + + + {t("credentials.sshPublicKey")} + +
+
+ { + const file = e.target.files?.[0]; + if (file) { + try { + const fileContent = await file.text(); + field.onChange(fileContent); + debouncedPublicKeyDetection(fileContent); + } catch (error) { + console.error( + "Failed to read uploaded public key file:", + error, + ); + } + } + }} + className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" + /> + +
+ +
+ + { + field.onChange(value); + debouncedPublicKeyDetection(value); + }} + placeholder={t("placeholders.pastePublicKey")} + theme={editorTheme} + className="border border-input rounded-md overflow-hidden" + minHeight="120px" + basicSetup={{ + lineNumbers: true, + foldGutter: false, + dropCursor: false, + allowMultipleSelections: false, + highlightSelectionMatches: false, + searchKeymap: false, + scrollPastEnd: false, + }} + extensions={[ + EditorView.theme({ + ".cm-scroller": { + overflow: "auto", + scrollbarWidth: "thin", + scrollbarColor: + "var(--scrollbar-thumb) var(--scrollbar-track)", + }, + }), + ]} + /> + + {detectedPublicKeyType && field.value && ( +
+ + {t("credentials.detectedKeyType")}:{" "} + + + {getFriendlyKeyTypeName(detectedPublicKeyType)} + + {publicKeyDetectionLoading && ( + + ({t("credentials.detectingKeyType")}) + + )} +
+ )} +
+ )} + /> +
+
+ ( + + {t("credentials.keyPassword")} + + + + + )} + /> +
+
+
+
+ + ); +} diff --git a/src/ui/desktop/apps/host-manager/credentials/tabs/CredentialGeneralTab.tsx b/src/ui/desktop/apps/host-manager/credentials/tabs/CredentialGeneralTab.tsx new file mode 100644 index 00000000..810612a1 --- /dev/null +++ b/src/ui/desktop/apps/host-manager/credentials/tabs/CredentialGeneralTab.tsx @@ -0,0 +1,237 @@ +import React, { useRef, useState, useEffect } from "react"; +import { + FormControl, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/form.tsx"; +import { Input } from "@/components/ui/input.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import type { Control, UseFormWatch } from "react-hook-form"; + +interface CredentialGeneralTabProps { + control: Control; + watch: UseFormWatch; + folders: string[]; + t: (key: string, params?: any) => string; +} + +export function CredentialGeneralTab({ + control, + watch, + folders, + t, +}: CredentialGeneralTabProps) { + const [tagInput, setTagInput] = useState(""); + const [folderDropdownOpen, setFolderDropdownOpen] = useState(false); + const folderInputRef = useRef(null); + const folderDropdownRef = useRef(null); + + const folderValue = 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, + onChange: (value: string) => void, + ) => { + onChange(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]); + + return ( + <> + + {t("credentials.basicInformation")} + +
+ ( + + {t("credentials.credentialName")} + + + + + )} + /> + + ( + + {t("credentials.username")} + + + + + )} + /> +
+ + {t("credentials.organization")} + +
+ ( + + {t("credentials.description")} + + + + + )} + /> + + ( + + {t("credentials.folder")} + + setFolderDropdownOpen(true)} + onChange={(e) => { + field.onChange(e); + setFolderDropdownOpen(true); + }} + /> + + {folderDropdownOpen && filteredFolders.length > 0 && ( +
+
+ {filteredFolders.map((folder) => ( + + ))} +
+
+ )} +
+ )} + /> + + ( + + {t("credentials.tags")} + +
+ {(field.value || []).map((tag: string, idx: number) => ( + + {tag} + + + ))} + setTagInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === " " && tagInput.trim() !== "") { + e.preventDefault(); + const currentTags = field.value || []; + if (!currentTags.includes(tagInput.trim())) { + field.onChange([...currentTags, tagInput.trim()]); + } + setTagInput(""); + } else if (e.key === "Enter" && tagInput.trim() !== "") { + e.preventDefault(); + const currentTags = field.value || []; + if (!currentTags.includes(tagInput.trim())) { + field.onChange([...currentTags, tagInput.trim()]); + } + setTagInput(""); + } else if ( + e.key === "Backspace" && + tagInput === "" && + (field.value || []).length > 0 + ) { + const currentTags = field.value || []; + field.onChange(currentTags.slice(0, -1)); + } + }} + placeholder={t("credentials.addTagsSpaceToAdd")} + /> +
+
+
+ )} + /> +
+ + ); +} diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/HostAuthenticationSection.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostAuthenticationSection.tsx new file mode 100644 index 00000000..686bd154 --- /dev/null +++ b/src/ui/desktop/apps/host-manager/hosts/tabs/HostAuthenticationSection.tsx @@ -0,0 +1,349 @@ +import React, { useRef, useState, useEffect } from "react"; +import { Controller } from "react-hook-form"; +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/form.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import { Switch } from "@/components/ui/switch.tsx"; +import { PasswordInput } from "@/components/ui/password-input.tsx"; +import { Alert, AlertDescription } from "@/components/ui/alert.tsx"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs.tsx"; +import CodeMirror from "@uiw/react-codemirror"; +import { EditorView } from "@codemirror/view"; +import { CredentialSelector } from "@/ui/desktop/apps/host-manager/credentials/CredentialSelector.tsx"; +import type { HostAuthenticationSectionProps } from "./shared/tab-types"; + +export function HostAuthenticationSection({ + control, + watch, + setValue, + credentials, + authTab, + setAuthTab, + keyInputMethod, + setKeyInputMethod, + editorTheme, + editingHost, + t, +}: HostAuthenticationSectionProps) { + const [keyTypeDropdownOpen, setKeyTypeDropdownOpen] = useState(false); + const keyTypeButtonRef = useRef(null); + const keyTypeDropdownRef = useRef(null); + + 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") }, + ]; + + 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]); + + return ( + { + const newAuthType = value as "password" | "key" | "credential" | "none"; + setAuthTab(newAuthType); + setValue("authType", newAuthType); + }} + className="flex-1 flex flex-col h-full min-h-0" + > + + + {t("hosts.password")} + + + {t("hosts.key")} + + + {t("hosts.credential")} + + + {t("hosts.none")} + + + + ( + + {t("hosts.password")} + + + + + )} + /> + + + { + setKeyInputMethod(value as "upload" | "paste"); + if (value === "upload") { + setValue("key", null); + } else { + 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={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("hosts.keyPassword")} + + + + + )} + /> + ( + + {t("hosts.keyType")} + +
+ + {keyTypeDropdownOpen && ( +
+
+ {keyTypeOptions.map((opt) => ( + + ))} +
+
+ )} +
+
+
+ )} + /> +
+
+ +
+ ( + + { + if (credential && !watch("overrideCredentialUsername")) { + setValue("username", credential.username); + } + }} + /> + + {t("hosts.credentialDescription")} + + + )} + /> + {watch("credentialId") && ( + ( + +
+ + {t("hosts.overrideCredentialUsername")} + + + {t("hosts.overrideCredentialUsernameDesc")} + +
+ + + +
+ )} + /> + )} +
+
+ + + + {t("hosts.noneAuthTitle")} +
{t("hosts.noneAuthDescription")}
+
{t("hosts.noneAuthDetails")}
+
+
+
+
+ ); +} diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/HostDockerTab.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostDockerTab.tsx new file mode 100644 index 00000000..09907831 --- /dev/null +++ b/src/ui/desktop/apps/host-manager/hosts/tabs/HostDockerTab.tsx @@ -0,0 +1,29 @@ +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/form.tsx"; +import { Switch } from "@/components/ui/switch.tsx"; +import type { HostDockerTabProps } from "./shared/tab-types"; + +export function HostDockerTab({ control, t }: HostDockerTabProps) { + return ( +
+ ( + + {t("hosts.enableDocker")} + + + + {t("hosts.enableDockerDesc")} + + )} + /> +
+ ); +} diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/HostFileManagerTab.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostFileManagerTab.tsx new file mode 100644 index 00000000..34f2ee06 --- /dev/null +++ b/src/ui/desktop/apps/host-manager/hosts/tabs/HostFileManagerTab.tsx @@ -0,0 +1,61 @@ +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/form.tsx"; +import { Switch } from "@/components/ui/switch.tsx"; +import { Input } from "@/components/ui/input.tsx"; +import type { HostFileManagerTabProps } from "./shared/tab-types"; + +export function HostFileManagerTab({ + control, + watch, + t, +}: HostFileManagerTabProps) { + return ( +
+ ( + + {t("hosts.enableFileManager")} + + + + + {t("hosts.enableFileManagerDesc")} + + + )} + /> + + {watch("enableFileManager") && ( +
+ ( + + {t("hosts.defaultPath")} + + { + field.onChange(e.target.value.trim()); + field.onBlur(); + }} + /> + + {t("hosts.defaultPathDesc")} + + )} + /> +
+ )} +
+ ); +} diff --git a/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx b/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx new file mode 100644 index 00000000..09824543 --- /dev/null +++ b/src/ui/desktop/apps/host-manager/hosts/tabs/HostGeneralTab.tsx @@ -0,0 +1,785 @@ +import React, { useRef, useState, useEffect } from "react"; +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/form.tsx"; +import { Input } from "@/components/ui/input.tsx"; +import { Button } from "@/components/ui/button.tsx"; +import { Switch } from "@/components/ui/switch.tsx"; +import { Textarea } from "@/components/ui/textarea.tsx"; +import { Separator } from "@/components/ui/separator.tsx"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion.tsx"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select.tsx"; +import { Alert, AlertDescription } from "@/components/ui/alert.tsx"; +import { Plus, X } from "lucide-react"; +import { PasswordInput } from "@/components/ui/password-input.tsx"; +import { JumpHostItem } from "./shared/JumpHostItem.tsx"; +import { HostAuthenticationSection } from "./HostAuthenticationSection.tsx"; +import type { HostGeneralTabProps } from "./shared/tab-types"; + +export function HostGeneralTab({ + control, + watch, + setValue, + getValues, + hosts, + credentials, + folders, + snippets, + editorTheme, + editingHost, + authTab, + setAuthTab, + keyInputMethod, + setKeyInputMethod, + proxyMode, + setProxyMode, + ipInputRef, + t, +}: HostGeneralTabProps) { + const [tagInput, setTagInput] = useState(""); + const [folderDropdownOpen, setFolderDropdownOpen] = useState(false); + const folderInputRef = useRef(null); + const folderDropdownRef = useRef(null); + + const folderValue = 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) => { + 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]); + + return ( + <> + + {t("hosts.connectionDetails")} + +
+ ( + + {t("hosts.ipAddress")} + + { + field.ref(e); + if (ipInputRef?.current) { + ipInputRef.current = e; + } + }} + onBlur={(e) => { + field.onChange(e.target.value.trim()); + field.onBlur(); + }} + /> + + + )} + /> + + ( + + {t("hosts.port")} + + + + + )} + /> + + { + const isCredentialAuth = authTab === "credential"; + const hasCredential = !!watch("credentialId"); + const overrideEnabled = !!watch("overrideCredentialUsername"); + const shouldDisable = + isCredentialAuth && hasCredential && !overrideEnabled; + + return ( + + {t("hosts.username")} + + { + field.onChange(e.target.value.trim()); + field.onBlur(); + }} + /> + + + ); + }} + /> +
+ + {t("hosts.organization")} + +
+ ( + + {t("hosts.name")} + + { + field.onChange(e.target.value.trim()); + field.onBlur(); + }} + /> + + + )} + /> + + ( + + {t("hosts.folder")} + + setFolderDropdownOpen(true)} + onChange={(e) => { + field.onChange(e); + setFolderDropdownOpen(true); + }} + onBlur={(e) => { + field.onChange(e.target.value.trim()); + field.onBlur(); + }} + /> + + {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")} + + + + + )} + /> + + ( + + {t("hosts.notes")} + +