diff --git a/data/network.json.example b/data/network.json.example index 1668705..9c570a2 100644 --- a/data/network.json.example +++ b/data/network.json.example @@ -64,5 +64,11 @@ "name": "LAN", "vlanId": 1 } + ], + "racks": [ + { + "name": "Example Rack", + "heightU": 24 + } ] } diff --git a/src/lib/components/NetworkDiagram.svelte b/src/lib/components/NetworkDiagram.svelte index d60142f..1f99cf3 100644 --- a/src/lib/components/NetworkDiagram.svelte +++ b/src/lib/components/NetworkDiagram.svelte @@ -15,6 +15,7 @@ deletePort, renameOwner, renamePort, + renameRack, setPortCable, setPortConnection, type OwnerKind, @@ -166,6 +167,108 @@ $: rackLayout = buildRackLayout(networkData); $: rackNames = knownRackNames(networkData); + let rackNewNameTarget: { kind: OwnerKind; index: number } | null = null; + + function onRackSelectChange(kind: OwnerKind, index: number, value: string) { + if (value === '__new__') { + rackNewNameTarget = { kind, index }; + return; + } + rackNewNameTarget = null; + updateRackField(kind, index, 'name', value); + } + + function submitNewRackName(value: string) { + const target = rackNewNameTarget; + rackNewNameTarget = null; + const name = value.trim(); + if (!target || !name) { + return; + } + mutateDraft( + (draft) => { + draft.racks ??= []; + if (!draft.racks.some((rack) => rack.name.toLowerCase() === name.toLowerCase())) { + draft.racks.push({ name }); + } + const entity = + target.kind === 'machine' ? draft.machines[target.index] : draft.devices[target.index]; + if (entity) { + entity.rack = { + name, + unit: entity.rack?.unit ?? 1, + ...(entity.rack?.heightU ? { heightU: entity.rack.heightU } : {}) + }; + } + }, + { autosave: true } + ); + } + + function renameRackDefinition(previousName: string, nextName: string) { + if (!nextName.trim() || nextName.trim() === previousName) { + return; + } + applyDraft(renameRack(networkData, previousName, nextName)); + } + + function setRackHeightU(rackName: string, value: string) { + const parsed = Number(value); + mutateDraft( + (draft) => { + draft.racks ??= []; + let definition = draft.racks.find( + (rack) => rack.name.toLowerCase() === rackName.toLowerCase() + ); + if (!definition) { + definition = { name: rackName }; + draft.racks.push(definition); + } + if (value.trim() && Number.isInteger(parsed) && parsed >= 1) { + definition.heightU = parsed; + } else { + delete definition.heightU; + } + }, + { autosave: true } + ); + } + + function addRackDefinition() { + const existing = new Set(rackNames.map((name) => name.toLowerCase())); + let candidate = 'New Rack'; + for (let index = 2; existing.has(candidate.toLowerCase()) && index < 100; index += 1) { + candidate = `New Rack ${index}`; + } + if (existing.has(candidate.toLowerCase())) { + return; + } + mutateDraft( + (draft) => { + draft.racks ??= []; + draft.racks.push({ name: candidate }); + }, + { autosave: true } + ); + } + + function removeRackDefinition(rackName: string) { + mutateDraft( + (draft) => { + if (!draft.racks) { + return; + } + draft.racks = draft.racks.filter( + (rack) => rack.name.toLowerCase() !== rackName.toLowerCase() + ); + if (draft.racks.length === 0) { + delete draft.racks; + } + }, + { autosave: true } + ); + } + $: ipamReport = buildIpamReport(networkData); $: ipamWarnings = ipamReport.duplicates.map( (duplicate) => `Duplicate IP ${duplicate.ip}: assigned to ${duplicate.owners.join(' and ')}.` @@ -1620,6 +1723,7 @@ function closeSelectedTargetModal() { selectedTarget = null; + rackNewNameTarget = null; clearIconSearch(); } @@ -2011,7 +2115,50 @@ {/if} - {#if vlanLegend.length > 0} + {#if diagramViewMode === 'rack'} +