adding ports fixed
This commit is contained in:
@@ -172,12 +172,26 @@ router.get('/:id/ports', (req, res) => {
|
|||||||
|
|
||||||
// POST /api/components/:id/ports
|
// POST /api/components/:id/ports
|
||||||
router.post('/:id/ports', (req, res) => {
|
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' });
|
if (!component) return res.status(404).json({ error: 'Component not found' });
|
||||||
|
|
||||||
const { port_number, label, port_type, connected_to_port_id, notes } = req.body;
|
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' });
|
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();
|
const id = uuidv4();
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO ports (id, component_id, port_number, label, port_type, connected_to_port_id, notes)
|
INSERT INTO ports (id, component_id, port_number, label, port_type, connected_to_port_id, notes)
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export default function ComponentPage() {
|
|||||||
const [portModal, setPortModal] = useState<PortModalState | null>(null);
|
const [portModal, setPortModal] = useState<PortModalState | null>(null);
|
||||||
const [portForm, setPortForm] = useState(BLANK_PORT_FORM);
|
const [portForm, setPortForm] = useState(BLANK_PORT_FORM);
|
||||||
const [patchPanels, setPatchPanels] = useState<Component[]>([]);
|
const [patchPanels, setPatchPanels] = useState<Component[]>([]);
|
||||||
|
const [switches, setSwitches] = useState<Component[]>([]);
|
||||||
|
const [swLink, setSwLink] = useState<{ switchId: string; portNumber: string; portId: string | null } | null>(null);
|
||||||
|
const [portError, setPortError] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadComponent = useCallback(async () => {
|
const loadComponent = useCallback(async () => {
|
||||||
if (!componentId) return;
|
if (!componentId) return;
|
||||||
@@ -39,6 +42,10 @@ export default function ComponentPage() {
|
|||||||
const all = await api.getComponents(c.rack.id);
|
const all = await api.getComponents(c.rack.id);
|
||||||
setPatchPanels(all.filter(comp => comp.type === 'patch_panel'));
|
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]);
|
}, [componentId]);
|
||||||
|
|
||||||
useEffect(() => { loadComponent(); }, [loadComponent]);
|
useEffect(() => { loadComponent(); }, [loadComponent]);
|
||||||
@@ -73,6 +80,7 @@ export default function ComponentPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openPortModal = (port?: Port) => {
|
const openPortModal = (port?: Port) => {
|
||||||
|
setPortError(null);
|
||||||
setPortModal({ editPort: port ?? null });
|
setPortModal({ editPort: port ?? null });
|
||||||
setPortForm(port ? {
|
setPortForm(port ? {
|
||||||
port_number: String(port.port_number),
|
port_number: String(port.port_number),
|
||||||
@@ -81,18 +89,64 @@ export default function ComponentPage() {
|
|||||||
notes: port.notes ?? '',
|
notes: port.notes ?? '',
|
||||||
connected_to_port_id: port.connected_to_port_id ?? '',
|
connected_to_port_id: port.connected_to_port_id ?? '',
|
||||||
} : BLANK_PORT_FORM);
|
} : 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) => {
|
const handleSavePort = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!componentId || !portForm.port_number) return;
|
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) {
|
if (portModal?.editPort) {
|
||||||
await api.updatePort(componentId, portModal.editPort.id, {
|
await api.updatePort(componentId, portModal.editPort.id, {
|
||||||
label: portForm.label || undefined,
|
label: portForm.label || undefined,
|
||||||
port_type: portForm.port_type,
|
port_type: portForm.port_type,
|
||||||
notes: portForm.notes || undefined,
|
notes: portForm.notes || undefined,
|
||||||
connected_to_port_id: portForm.connected_to_port_id || null,
|
connected_to_port_id: connectedToPortId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await api.createPort(componentId, {
|
await api.createPort(componentId, {
|
||||||
@@ -100,10 +154,12 @@ export default function ComponentPage() {
|
|||||||
label: portForm.label || undefined,
|
label: portForm.label || undefined,
|
||||||
port_type: portForm.port_type,
|
port_type: portForm.port_type,
|
||||||
notes: portForm.notes || undefined,
|
notes: portForm.notes || undefined,
|
||||||
connected_to_port_id: portForm.connected_to_port_id || undefined,
|
connected_to_port_id: connectedToPortId ?? undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setPortModal(null);
|
setPortModal(null);
|
||||||
|
setSwLink(null);
|
||||||
|
setPortError(null);
|
||||||
await loadComponent();
|
await loadComponent();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -324,11 +380,16 @@ export default function ComponentPage() {
|
|||||||
<label>{component.type === 'patch_panel' ? 'PP Port #' : 'SW Port #'}</label>
|
<label>{component.type === 'patch_panel' ? 'PP Port #' : 'SW Port #'}</label>
|
||||||
<input
|
<input
|
||||||
type="number" className="form-input"
|
type="number" className="form-input"
|
||||||
|
min={1}
|
||||||
|
max={component.type === 'patch_panel' && component.port_count ? component.port_count : undefined}
|
||||||
value={portForm.port_number}
|
value={portForm.port_number}
|
||||||
onChange={e => setPortForm(f => ({ ...f, port_number: e.target.value }))}
|
onChange={e => setPortForm(f => ({ ...f, port_number: e.target.value }))}
|
||||||
required
|
required
|
||||||
disabled={!!portModal.editPort}
|
disabled={!!portModal.editPort}
|
||||||
/>
|
/>
|
||||||
|
{component.type === 'patch_panel' && component.port_count && !portModal.editPort && (
|
||||||
|
<span className="form-hint">Port 1–{component.port_count}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group form-group-lg">
|
<div className="form-group form-group-lg">
|
||||||
<label>{component.type === 'patch_panel' ? 'End Device' : 'Label'}</label>
|
<label>{component.type === 'patch_panel' ? 'End Device' : 'Label'}</label>
|
||||||
@@ -374,6 +435,21 @@ export default function ComponentPage() {
|
|||||||
onChange={id => setPortForm(f => ({ ...f, connected_to_port_id: id }))}
|
onChange={id => setPortForm(f => ({ ...f, connected_to_port_id: id }))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{component.type === 'patch_panel' && (
|
||||||
|
<SwLinkPicker
|
||||||
|
key={portModal.editPort?.id ?? 'add'}
|
||||||
|
switches={switches}
|
||||||
|
initialSwitchId={swLink?.switchId}
|
||||||
|
initialPortNumber={swLink?.portNumber}
|
||||||
|
currentLinkedPortId={swLink?.portId}
|
||||||
|
onChange={setSwLink}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{portError && (
|
||||||
|
<div style={{ color: '#f87171', fontSize: 13, background: '#450a0a', border: '1px solid #7f1d1d', borderRadius: 6, padding: '8px 12px' }}>
|
||||||
|
⚠ {portError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="form-actions">
|
<div className="form-actions">
|
||||||
<button type="submit" className="btn-primary btn-sm">
|
<button type="submit" className="btn-primary btn-sm">
|
||||||
{portModal.editPort ? 'Save Changes' : (component.type === 'patch_panel' ? 'Add PP Port' : 'Add SW Port')}
|
{portModal.editPort ? 'Save Changes' : (component.type === 'patch_panel' ? 'Add PP Port' : 'Add SW Port')}
|
||||||
@@ -477,3 +553,95 @@ function PpLinkPicker({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<Component | null>(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 (
|
||||||
|
<div className="pp-link-picker">
|
||||||
|
<span className="pp-link-label">🔗 Link to Switch Port <span style={{ color: 'var(--text3)', fontWeight: 400 }}>(optional)</span></span>
|
||||||
|
<div className="pp-link-row">
|
||||||
|
<select
|
||||||
|
className="form-input form-input-sm"
|
||||||
|
value={selectedSwitchId}
|
||||||
|
onChange={e => handleSwitchChange(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">— no switch —</option>
|
||||||
|
{switches.map(sw => (
|
||||||
|
<option key={sw.id} value={sw.id}>{sw.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{selectedSwitchId && !loadingSwitch && switchData && (
|
||||||
|
<select
|
||||||
|
className="form-input form-input-sm"
|
||||||
|
value={selectedPortNumber}
|
||||||
|
onChange={e => handlePortChange(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">— select port —</option>
|
||||||
|
{Array.from({ length: portCount }, (_, i) => i + 1).map(n => {
|
||||||
|
const existingPort = (switchData.ports ?? []).find(p => p.port_number === n);
|
||||||
|
const isOccupied = existingPort?.connected_to_port_id && existingPort.id !== currentLinkedPortId;
|
||||||
|
return (
|
||||||
|
<option key={n} value={String(n)} disabled={!!isOccupied}>
|
||||||
|
Port {n}{existingPort?.label ? ` — ${existingPort.label}` : ''}{isOccupied ? ' (in use)' : ''}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
{loadingSwitch && <span style={{ color: 'var(--text3)', fontSize: 12 }}>Loading…</span>}
|
||||||
|
{switches.length === 0 && (
|
||||||
|
<span style={{ color: 'var(--text3)', fontSize: 12 }}>No switches in this rack</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{selectedSwitchId && selectedPortNumber && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="pp-link-clear"
|
||||||
|
onClick={() => { setSelectedSwitchId(''); setSelectedPortNumber(''); onChange(null); }}
|
||||||
|
>
|
||||||
|
✕ Clear link
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user