import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { ChevronsUpDown, Search, Server, X } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/popover"; import type { Host } from "@/types/ui-types"; /** * Walks parentHostId to collect every descendant id of `hostId`, so a host * can never be assigned under its own descendant (which would form a cycle). * Mirrors the ancestor-walk cycle guard used server-side and in buildHostTree. */ export function collectDescendantIds( hostId: string, hosts: Host[], ): Set { const childrenByParent = new Map(); for (const h of hosts) { if (!h.parentHostId) continue; const list = childrenByParent.get(h.parentHostId) ?? []; list.push(h.id); childrenByParent.set(h.parentHostId, list); } const out = new Set(); const stack = [...(childrenByParent.get(hostId) ?? [])]; while (stack.length > 0) { const id = stack.pop()!; if (out.has(id)) continue; out.add(id); stack.push(...(childrenByParent.get(id) ?? [])); } return out; } export function HostParentPicker({ value, onChange, hosts, excludeHostId, }: { value: string; onChange: (hostId: string) => void; hosts: Host[]; /** The host being edited, if any -- excluded from the list along with its descendants. */ excludeHostId?: string | null; }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); const excludedIds = useMemo(() => { if (!excludeHostId) return new Set(); const descendants = collectDescendantIds(excludeHostId, hosts); descendants.add(excludeHostId); return descendants; }, [excludeHostId, hosts]); const candidates = useMemo( () => hosts .filter((h) => !excludedIds.has(h.id)) .sort((a, b) => a.name.localeCompare(b.name)), [hosts, excludedIds], ); const query = search.trim().toLowerCase(); const filtered = query ? candidates.filter( (h) => h.name.toLowerCase().includes(query) || h.ip.toLowerCase().includes(query), ) : candidates; const selected = value ? hosts.find((h) => h.id === value) : undefined; function commit(hostId: string) { onChange(hostId); setSearch(""); setOpen(false); } return (
setSearch(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && filtered.length > 0) { e.preventDefault(); commit(filtered[0].id); } }} placeholder={t("hosts.parentHostPickerSearch")} className="flex-1 text-xs bg-transparent outline-none placeholder:text-muted-foreground/50 text-foreground min-w-0" />
{value && ( )} {filtered.map((h) => ( ))} {filtered.length === 0 && (
{t("hosts.parentHostPickerEmpty")}
)}
); }