import { useState, useEffect, type FormEvent } from 'react'; import { toast } from 'sonner'; import type { Port, Vlan, VlanMode } from '../../types'; import { Modal } from '../ui/Modal'; import { Button } from '../ui/Button'; import { Badge } from '../ui/Badge'; import { apiClient } from '../../api/client'; import { useRackStore } from '../../store/useRackStore'; interface PortConfigModalProps { portId: string; open: boolean; onClose: () => void; } export function PortConfigModal({ portId, open, onClose }: PortConfigModalProps) { const { racks, updateModuleLocal } = useRackStore(); const [port, setPort] = useState(null); const [vlans, setVlans] = useState([]); const [label, setLabel] = useState(''); const [mode, setMode] = useState('ACCESS'); const [nativeVlanId, setNativeVlanId] = useState(''); const [taggedVlanIds, setTaggedVlanIds] = useState([]); const [notes, setNotes] = useState(''); const [loading, setLoading] = useState(false); const [fetching, setFetching] = useState(false); // Quick-create VLAN const [newVlanId, setNewVlanId] = useState(''); const [newVlanName, setNewVlanName] = useState(''); const [creatingVlan, setCreatingVlan] = useState(false); // Find the port from store useEffect(() => { if (!open) return; let found: Port | undefined; for (const rack of racks) { for (const mod of rack.modules) { found = mod.ports.find((p) => p.id === portId); if (found) break; } if (found) break; } if (found) { setPort(found); setLabel(found.label ?? ''); setMode(found.mode); setNativeVlanId(found.nativeVlan?.toString() ?? ''); setTaggedVlanIds(found.vlans.filter((v) => v.tagged).map((v) => v.vlanId)); setNotes(found.notes ?? ''); } }, [open, portId, racks]); // Load VLAN list useEffect(() => { if (!open) return; setFetching(true); apiClient.vlans .list() .then(setVlans) .catch(() => toast.error('Failed to load VLANs')) .finally(() => setFetching(false)); }, [open]); async function handleSubmit(e: FormEvent) { e.preventDefault(); setLoading(true); try { const vlanAssignments = [ ...(mode === 'ACCESS' && nativeVlanId ? [{ vlanId: vlans.find((v) => v.vlanId === Number(nativeVlanId))?.id ?? '', tagged: false }] : []), ...(mode !== 'ACCESS' ? taggedVlanIds.map((id) => ({ vlanId: id, tagged: true })) : []), ].filter((v) => v.vlanId); await apiClient.ports.update(portId, { label: label.trim() || undefined, mode, nativeVlan: nativeVlanId ? Number(nativeVlanId) : null, notes: notes.trim() || undefined, vlans: vlanAssignments, }); // Refresh racks to reflect changes const portsInRack = racks.flatMap((r) => r.modules).find((m) => m.ports.some((p) => p.id === portId)); if (portsInRack) { const updatedPorts = await apiClient.modules.getPorts(portsInRack.id); updateModuleLocal(portsInRack.id, { ports: updatedPorts }); } toast.success('Port saved'); onClose(); } catch (err) { toast.error(err instanceof Error ? err.message : 'Save failed'); } finally { setLoading(false); } } async function handleCreateVlan() { const id = Number(newVlanId); if (!id || !newVlanName.trim()) return; setCreatingVlan(true); try { const created = await apiClient.vlans.create({ vlanId: id, name: newVlanName.trim() }); setVlans((v) => [...v, created].sort((a, b) => a.vlanId - b.vlanId)); setNewVlanId(''); setNewVlanName(''); toast.success(`VLAN ${id} created`); } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to create VLAN'); } finally { setCreatingVlan(false); } } function toggleTaggedVlan(vlanDbId: string) { setTaggedVlanIds((prev) => prev.includes(vlanDbId) ? prev.filter((id) => id !== vlanDbId) : [...prev, vlanDbId] ); } if (!port) return null; return (
{/* Port info */}
{port.portType} Port #{port.portNumber}
{/* Label */}
setLabel(e.target.value)} disabled={loading} placeholder="e.g. Server 1 uplink" className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-sm text-slate-100 placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50" />
{/* Mode */}
{(['ACCESS', 'TRUNK', 'HYBRID'] as VlanMode[]).map((m) => ( ))}
{/* Native VLAN */}
{/* Tagged VLANs — Trunk/Hybrid only */} {mode !== 'ACCESS' && (
{vlans.length === 0 && ( No VLANs defined yet )} {vlans.map((v) => ( ))}
)} {/* Quick-create VLAN */}

Quick-create VLAN

setNewVlanId(e.target.value)} placeholder="VLAN ID" className="w-24 bg-slate-900 border border-slate-600 rounded px-2 py-1.5 text-xs text-slate-100 placeholder:text-slate-600 focus:outline-none focus:ring-1 focus:ring-blue-500" /> setNewVlanName(e.target.value)} placeholder="Name" className="flex-1 bg-slate-900 border border-slate-600 rounded px-2 py-1.5 text-xs text-slate-100 placeholder:text-slate-600 focus:outline-none focus:ring-1 focus:ring-blue-500" />
{/* Notes */}