From 4d61374b7ca49a0ec6a379ca91f92a8eb9f69c02 Mon Sep 17 00:00:00 2001 From: scheianu Date: Sun, 10 May 2026 14:42:57 +0300 Subject: [PATCH] adding ports fixed --- backend/src/routes/components.js | 16 ++- frontend/src/pages/ComponentPage.tsx | 174 ++++++++++++++++++++++++++- 2 files changed, 186 insertions(+), 4 deletions(-) diff --git a/backend/src/routes/components.js b/backend/src/routes/components.js index 5c9fd5a..c4bceaf 100644 --- a/backend/src/routes/components.js +++ b/backend/src/routes/components.js @@ -172,12 +172,26 @@ router.get('/:id/ports', (req, res) => { // POST /api/components/:id/ports router.post('/:id/ports', (req, res) => { - const component = db.prepare('SELECT id FROM components WHERE id = ?').get(req.params.id); + const component = db.prepare('SELECT id, type, port_count FROM components WHERE id = ?').get(req.params.id); if (!component) return res.status(404).json({ error: 'Component not found' }); const { port_number, label, port_type, connected_to_port_id, notes } = req.body; if (port_number == null) return res.status(400).json({ error: 'port_number is required' }); + // Patch-panel specific: no duplicate port numbers, enforce port_count capacity + if (component.type === 'patch_panel') { + const existing = db.prepare('SELECT id FROM ports WHERE component_id = ? AND port_number = ?').get(req.params.id, port_number); + if (existing) return res.status(409).json({ error: `Port ${port_number} already exists on this patch panel.` }); + + if (component.port_count != null) { + const count = db.prepare('SELECT COUNT(*) as n FROM ports WHERE component_id = ?').get(req.params.id).n; + if (count >= component.port_count) + return res.status(409).json({ error: `Patch panel is full (${component.port_count} ports maximum).` }); + if (port_number < 1 || port_number > component.port_count) + return res.status(400).json({ error: `Port number must be between 1 and ${component.port_count}.` }); + } + } + const id = uuidv4(); db.prepare(` INSERT INTO ports (id, component_id, port_number, label, port_type, connected_to_port_id, notes) diff --git a/frontend/src/pages/ComponentPage.tsx b/frontend/src/pages/ComponentPage.tsx index 196db37..6134bac 100644 --- a/frontend/src/pages/ComponentPage.tsx +++ b/frontend/src/pages/ComponentPage.tsx @@ -21,6 +21,9 @@ export default function ComponentPage() { const [portModal, setPortModal] = useState(null); const [portForm, setPortForm] = useState(BLANK_PORT_FORM); const [patchPanels, setPatchPanels] = useState([]); + const [switches, setSwitches] = useState([]); + const [swLink, setSwLink] = useState<{ switchId: string; portNumber: string; portId: string | null } | null>(null); + const [portError, setPortError] = useState(null); const loadComponent = useCallback(async () => { if (!componentId) return; @@ -39,6 +42,10 @@ export default function ComponentPage() { const all = await api.getComponents(c.rack.id); setPatchPanels(all.filter(comp => comp.type === 'patch_panel')); } + if (c.type === 'patch_panel' && c.rack?.id) { + const all = await api.getComponents(c.rack.id); + setSwitches(all.filter(comp => comp.type === 'switch')); + } }, [componentId]); useEffect(() => { loadComponent(); }, [loadComponent]); @@ -73,6 +80,7 @@ export default function ComponentPage() { }; const openPortModal = (port?: Port) => { + setPortError(null); setPortModal({ editPort: port ?? null }); setPortForm(port ? { port_number: String(port.port_number), @@ -81,18 +89,64 @@ export default function ComponentPage() { notes: port.notes ?? '', connected_to_port_id: port.connected_to_port_id ?? '', } : BLANK_PORT_FORM); + if (port?.linked_port?.component_type === 'switch') { + setSwLink({ switchId: port.linked_port.component_id, portNumber: String(port.linked_port.port_number), portId: port.linked_port.id }); + } else { + setSwLink(null); + } }; - const closePortModal = () => setPortModal(null); + const closePortModal = () => { setPortModal(null); setSwLink(null); setPortError(null); }; const handleSavePort = async (e: React.FormEvent) => { e.preventDefault(); if (!componentId || !portForm.port_number) return; + setPortError(null); + + // Patch-panel only: validate no duplicate port number and capacity limit + if (component?.type === 'patch_panel' && !portModal?.editPort) { + const existingPorts: Port[] = component.ports ?? []; + const portNum = Number(portForm.port_number); + const maxPorts = component.port_count ?? Infinity; + + if (existingPorts.some(p => p.port_number === portNum)) { + setPortError(`Port ${portNum} is already added. Each port number can only appear once.`); + return; + } + if (existingPorts.length >= maxPorts) { + setPortError(`This patch panel is full (${maxPorts} ports). Delete an existing port to add a new one.`); + return; + } + if (portNum < 1 || portNum > maxPorts) { + setPortError(`Port number must be between 1 and ${maxPorts}.`); + return; + } + } + + // Resolve connected_to_port_id for patch panels via swLink + let connectedToPortId: string | null | undefined = portForm.connected_to_port_id || null; + if (component?.type === 'patch_panel') { + if (swLink?.switchId && swLink?.portNumber) { + if (swLink.portId) { + connectedToPortId = swLink.portId; + } else { + // Switch port record doesn't exist yet – create it first + const newSwPort = await api.createPort(swLink.switchId, { + port_number: Number(swLink.portNumber), + port_type: 'RJ45', + }); + connectedToPortId = newSwPort.id; + } + } else { + connectedToPortId = null; + } + } + if (portModal?.editPort) { await api.updatePort(componentId, portModal.editPort.id, { label: portForm.label || undefined, port_type: portForm.port_type, notes: portForm.notes || undefined, - connected_to_port_id: portForm.connected_to_port_id || null, + connected_to_port_id: connectedToPortId, }); } else { await api.createPort(componentId, { @@ -100,10 +154,12 @@ export default function ComponentPage() { label: portForm.label || undefined, port_type: portForm.port_type, notes: portForm.notes || undefined, - connected_to_port_id: portForm.connected_to_port_id || undefined, + connected_to_port_id: connectedToPortId ?? undefined, }); } setPortModal(null); + setSwLink(null); + setPortError(null); await loadComponent(); }; @@ -324,11 +380,16 @@ export default function ComponentPage() { setPortForm(f => ({ ...f, port_number: e.target.value }))} required disabled={!!portModal.editPort} /> + {component.type === 'patch_panel' && component.port_count && !portModal.editPort && ( + Port 1–{component.port_count} + )}
@@ -374,6 +435,21 @@ export default function ComponentPage() { onChange={id => setPortForm(f => ({ ...f, connected_to_port_id: id }))} /> )} + {component.type === 'patch_panel' && ( + + )} + {portError && ( +
+ ⚠ {portError} +
+ )}
); } + +function SwLinkPicker({ + switches, + initialSwitchId, + initialPortNumber, + currentLinkedPortId, + onChange, +}: { + switches: Component[]; + initialSwitchId?: string; + initialPortNumber?: string; + currentLinkedPortId?: string | null; + onChange: (link: { switchId: string; portNumber: string; portId: string | null } | null) => void; +}) { + const [selectedSwitchId, setSelectedSwitchId] = useState(initialSwitchId ?? ''); + const [switchData, setSwitchData] = useState(null); + const [loadingSwitch, setLoadingSwitch] = useState(false); + const [selectedPortNumber, setSelectedPortNumber] = useState(initialPortNumber ?? ''); + + useEffect(() => { + if (!selectedSwitchId) { setSwitchData(null); return; } + setLoadingSwitch(true); + api.getComponent(selectedSwitchId).then(sw => { + setSwitchData(sw); + setLoadingSwitch(false); + }); + }, [selectedSwitchId]); + + const portCount = switchData?.port_count ?? 24; + + const handleSwitchChange = (swId: string) => { + setSelectedSwitchId(swId); + setSelectedPortNumber(''); + onChange(null); + }; + + const handlePortChange = (portNumberStr: string) => { + setSelectedPortNumber(portNumberStr); + if (!portNumberStr || !selectedSwitchId) { onChange(null); return; } + const existingPort = (switchData?.ports ?? []).find(p => p.port_number === Number(portNumberStr)); + onChange({ switchId: selectedSwitchId, portNumber: portNumberStr, portId: existingPort?.id ?? null }); + }; + + return ( +
+ πŸ”— Link to Switch Port (optional) +
+ + {selectedSwitchId && !loadingSwitch && switchData && ( + + )} + {loadingSwitch && Loading…} + {switches.length === 0 && ( + No switches in this rack + )} +
+ {selectedSwitchId && selectedPortNumber && ( + + )} +
+ ); +}