import { createElement, useState } from 'react' import { RotateCcw, ChevronDown } from 'lucide-react' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } from '@/components/ui/select' import { NODE_TYPE_LABELS, type NodeData, type NodeType, type CheckMethod } from '@/types' import { resolveNodeColors } from '@/utils/nodeColors' import { ICON_REGISTRY, ICON_CATEGORIES } from '@/utils/nodeIcons' const NODE_TYPE_GROUPS: { label: string; types: NodeType[] }[] = [ { label: 'Hardware', types: ['isp', 'router', 'switch', 'server', 'nas', 'ap', 'printer'] }, { label: 'Virtualization', types: ['proxmox', 'vm', 'lxc', 'docker'] }, { label: 'IoT', types: ['iot', 'camera', 'cpl'] }, { label: 'Generic', types: ['computer', 'generic', 'groupRect'] }, ] const CHECK_METHODS: CheckMethod[] = ['none', 'ping', 'http', 'https', 'tcp', 'ssh', 'prometheus', 'health'] const DEFAULT_DATA: Partial = { type: 'server', label: '', hostname: '', ip: '', status: 'unknown', check_method: 'ping', services: [], container_mode: true, custom_colors: undefined, custom_icon: undefined, } interface NodeModalProps { open: boolean onClose: () => void onSubmit: (data: Partial) => void initial?: Partial title?: string proxmoxNodes?: { id: string; label: string }[] } const CHILD_TYPES: NodeType[] = ['vm', 'lxc'] // NodeModal is always mounted with a key that changes on open/edit, so useState // initial value is enough — no need for a reset effect. export function NodeModal({ open, onClose, onSubmit, initial, title = 'Add Node', proxmoxNodes = [] }: NodeModalProps) { const [form, setForm] = useState>({ ...DEFAULT_DATA, ...initial }) const [iconSearch, setIconSearch] = useState('') const [iconPickerOpen, setIconPickerOpen] = useState(false) const [labelError, setLabelError] = useState(false) const hasHardwareData = !!(initial?.cpu_count || initial?.cpu_model || initial?.ram_gb || initial?.disk_gb) const [hardwareOpen, setHardwareOpen] = useState(hasHardwareData) const set = (key: keyof NodeData, value: unknown) => setForm((f) => ({ ...f, [key]: value })) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() if (!form.label?.trim()) { setLabelError(true) return } setLabelError(false) onSubmit(form) onClose() } return ( !o && onClose()}> {title}
{/* Type */}
{/* Icon */}
{form.custom_icon && ( )}
{/* Trigger button */} {/* Inline picker panel */} {iconPickerOpen && (
setIconSearch(e.target.value)} placeholder="Search icons…" className="bg-[#21262d] border-[#30363d] text-xs h-7" autoFocus />
{ICON_CATEGORIES.map((cat) => { const entries = ICON_REGISTRY.filter( (e) => e.category === cat && (iconSearch === '' || e.label.toLowerCase().includes(iconSearch.toLowerCase()) || e.key.includes(iconSearch.toLowerCase())) ) if (entries.length === 0) return null return (

{cat}

{entries.map((entry) => { const isSelected = form.custom_icon === entry.key return ( ) })}
) })}
)}
{/* Label */}
{ set('label', e.target.value); if (labelError) setLabelError(false) }} placeholder="My Server" className={`bg-[#21262d] text-sm h-8 ${labelError ? 'border-[#f85149] focus-visible:ring-[#f85149]' : 'border-[#30363d]'}`} /> {labelError &&

Label is required

}
{/* Hostname */}
set('hostname', e.target.value)} placeholder="server.lan" className="bg-[#21262d] border-[#30363d] font-mono text-sm h-8" />
{/* IP */}
set('ip', e.target.value)} placeholder="192.168.1.x" className="bg-[#21262d] border-[#30363d] font-mono text-sm h-8" />
{/* Check method */}
{/* Check target */}
set('check_target', e.target.value)} placeholder="http://..." className="bg-[#21262d] border-[#30363d] font-mono text-sm h-8" />
{/* Parent Proxmox (VM / LXC only) */} {CHILD_TYPES.includes(form.type as NodeType) && proxmoxNodes.length > 0 && (
)} {/* Container mode (proxmox only) */} {form.type === 'proxmox' && (
Show VM/LXC nodes nested inside
)} {/* Appearance */}
{form.custom_colors && ( )}
{(['border', 'background', 'icon'] as const).map((key) => { const resolved = resolveNodeColors({ type: form.type ?? 'generic', custom_colors: form.custom_colors }) const currentValue = resolved[key] const isCustom = !!form.custom_colors?.[key] return (
{!form.custom_colors && (

Using default colors for {NODE_TYPE_LABELS[form.type ?? 'generic']}. Click a swatch to customize.

)}
{/* Hardware specs (hidden for groupRect) */} {form.type !== 'groupRect' && (
{hardwareOpen && (
Show on node
)}
{hardwareOpen && (
set('cpu_model', e.target.value || undefined)} placeholder="e.g. Intel Xeon E5-2680" className="bg-[#21262d] border-[#30363d] text-sm h-8" />
set('cpu_count', e.target.value ? parseInt(e.target.value, 10) : undefined)} placeholder="e.g. 8" className="bg-[#21262d] border-[#30363d] font-mono text-sm h-8" />
set('ram_gb', e.target.value ? parseFloat(e.target.value) : undefined)} placeholder="e.g. 32" className="bg-[#21262d] border-[#30363d] font-mono text-sm h-8" />
set('disk_gb', e.target.value ? parseFloat(e.target.value) : undefined)} placeholder="e.g. 500" className="bg-[#21262d] border-[#30363d] font-mono text-sm h-8" />
)}
)} {/* Notes */}
set('notes', e.target.value)} placeholder="Optional notes" className="bg-[#21262d] border-[#30363d] text-sm h-8" />
) }