import React from "react"; import type { ProxyNode } from "@/types/index"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/button"; import { Input } from "@/components/input"; import { PasswordInput } from "@/components/password-input"; import { FakeSwitch, SectionCard, SettingRow } from "@/components/section-card"; import type { Host } from "@/types/ui-types"; import { Activity, Globe, Plus, Tag, Terminal, Trash2, X } from "lucide-react"; import { FolderPathPicker } from "./FolderPathPicker"; import { HostParentPicker } from "./HostParentPicker"; import { getSSHFolders, isElectron } from "@/main-axios"; import { connectionOriginAppliesTo } from "./HostEditorData"; import { useSyncStatus } from "@/hooks/use-sync-status"; import type { HostEditorForm, HostProtocols } from "./HostEditorData"; import { Select2 } from "@/components/select2"; import { useHostProtocols } from "./host-protocols"; type HostEditorSetField = ( key: K, value: HostEditorForm[K], ) => void; export function HostEditorGeneralTab({ form, setField, protocols, handleProtocolToggle, hosts, host, simpleMode = false, }: { form: HostEditorForm; setField: HostEditorSetField; protocols: HostProtocols; handleProtocolToggle: (proto: keyof HostProtocols, value: boolean) => void; hosts: Host[]; host: Host | null; /** Hides organizational/advanced fields; their values still save unchanged. */ simpleMode?: boolean; }) { const { t } = useTranslation(); const syncLinked = !!useSyncStatus()?.linked; const pluginProtocols = useHostProtocols(); // Tracks which picker is shown, independent of whether a value is set yet // -- switching to "parent host" mode with nothing picked shouldn't bounce // back to the folder picker just because parentHostId is still empty. const [placementMode, setPlacementMode] = React.useState< "folder" | "parentHost" >(form.parentHostId ? "parentHost" : "folder"); const [folderMeta, setFolderMeta] = React.useState< Map >(new Map()); React.useEffect(() => { let cancelled = false; const load = () => { getSSHFolders() .then((folders) => { if (cancelled) return; const map = new Map(); for (const f of folders) { map.set(f.name, { color: f.color ?? undefined, icon: f.icon ?? undefined, }); } setFolderMeta(map); }) .catch(() => {}); }; load(); window.addEventListener("termix:hosts-changed", load); return () => { cancelled = true; window.removeEventListener("termix:hosts-changed", load); }; }, []); // Folders come from two sources: paths referenced by existing hosts, and // standalone folder records (including empty ones just created). // Intermediate ancestor paths are expanded so "A" appears even when only // "A / B" is directly stored on a host. const folderPaths = React.useMemo(() => { const set = new Set(); const addWithAncestors = (path: string) => { const parts = path.split(" / "); let accumulated = ""; for (const part of parts) { accumulated = accumulated ? `${accumulated} / ${part}` : part; set.add(accumulated); } }; for (const h of hosts) { if (h.folder) addWithAncestors(h.folder); } for (const path of folderMeta.keys()) addWithAncestors(path); return [...set]; }, [hosts, folderMeta]); return ( <> {/* Protocols — enable/disable each connection type */} } >
{[ { proto: "enableSsh", label: t("hosts.tabSsh"), desc: t("hosts.secureShell"), icon: , }, ...pluginProtocols.map((protocol) => { const Icon = protocol.icon; return { proto: protocol.settingKey, label: t(protocol.titleKey), desc: protocol.descriptionKey ? t(protocol.descriptionKey) : "", icon: , }; }), ].map(({ proto, label, desc, icon }) => { const enabled = !!protocols[proto]; return (
{icon}
{label} {desc}
handleProtocolToggle(proto, v)} />
); })}
} >
setField("ip", e.target.value)} />
setField("name", e.target.value)} />
{!Object.values(protocols).some(Boolean) && (
{t("hosts.enableAtLeastOneProtocol")}
)} } className={simpleMode ? "hidden" : undefined} >
{placementMode === "parentHost" ? ( setField("parentHostId", hostId)} hosts={hosts} excludeHostId={host?.id} /> ) : ( setField("folder", path)} folderPaths={folderPaths} folderMeta={folderMeta} /> )}
{form.tags.map((tag) => ( {tag} ))} setField("tagInput", e.target.value)} onKeyDown={(e) => { if ( (e.key === " " || e.key === "Enter") && form.tagInput.trim() ) { e.preventDefault(); const tag = form.tagInput.trim(); if (!form.tags.includes(tag)) setField("tags", [...form.tags, tag]); setField("tagInput", ""); } else if ( e.key === "Backspace" && !form.tagInput && form.tags.length > 0 ) { setField("tags", form.tags.slice(0, -1)); } }} />