diff --git a/data/network.json.example b/data/network.json.example
index 69c9004..1668705 100644
--- a/data/network.json.example
+++ b/data/network.json.example
@@ -42,6 +42,10 @@
"ipAddress": "192.168.1.2",
"type": "Network Switch",
"notes": "Optional notes",
+ "rack": {
+ "name": "Example Rack",
+ "unit": 12
+ },
"ports": [
{
"portName": "1",
diff --git a/src/lib/components/NetworkDiagram.svelte b/src/lib/components/NetworkDiagram.svelte
index 0f9998f..d60142f 100644
--- a/src/lib/components/NetworkDiagram.svelte
+++ b/src/lib/components/NetworkDiagram.svelte
@@ -23,6 +23,8 @@
import loadNetworkData from '$lib/data/loadNetworkData';
import { buildIpamReport, suggestNextFreeIp, type IpamReport } from '$lib/data/ipam';
import { vlanColor } from '$lib/graph/vlanPalette';
+ import { buildRackLayout, knownRackNames } from '$lib/data/rackLayout';
+ import RackView from './RackView.svelte';
import { validateNetworkData, type ValidationIssue } from '$lib/data/networkSchema';
import transformNetworkDataToGraph from '$lib/graph/transformNetworkData';
import { computeSearchMatches } from '$lib/graph/searchHighlight';
@@ -34,7 +36,7 @@
export let jsonPath = '/data/network.json';
type SaveState = 'saved' | 'saving' | 'unsaved' | 'error';
- type DiagramViewMode = 'network' | 'device';
+ type DiagramViewMode = 'network' | 'device' | 'rack';
type Position = { x: number; y: number };
type AddModalKind = 'machine' | 'device' | null;
type DeleteTarget =
@@ -161,6 +163,9 @@
applyEmphasis();
}
+ $: rackLayout = buildRackLayout(networkData);
+ $: rackNames = knownRackNames(networkData);
+
$: ipamReport = buildIpamReport(networkData);
$: ipamWarnings = ipamReport.duplicates.map(
(duplicate) => `Duplicate IP ${duplicate.ip}: assigned to ${duplicate.owners.join(' and ')}.`
@@ -888,8 +893,10 @@
function onDiagramViewSelectChange(event: Event) {
const nextValue = (event.currentTarget as HTMLSelectElement).value;
- diagramViewMode = nextValue === 'device' ? 'device' : 'network';
- refreshGraph();
+ diagramViewMode = nextValue === 'device' || nextValue === 'rack' ? nextValue : 'network';
+ if (diagramViewMode !== 'rack') {
+ refreshGraph();
+ }
}
function isHostCollapsed(hostId: string): boolean {
@@ -1424,6 +1431,47 @@
updateDevicePortConnection(deviceIndex, portIndex, next);
}
+ function updateRackField(
+ kind: OwnerKind,
+ index: number,
+ field: 'name' | 'unit' | 'heightU',
+ value: string
+ ) {
+ mutateDraft(
+ (draft) => {
+ const entity = kind === 'machine' ? draft.machines[index] : draft.devices[index];
+ if (!entity) {
+ return;
+ }
+ if (field === 'name') {
+ const name = value.trim();
+ if (!name) {
+ delete entity.rack;
+ return;
+ }
+ entity.rack = {
+ name,
+ unit: entity.rack?.unit ?? 1,
+ ...(entity.rack?.heightU ? { heightU: entity.rack.heightU } : {})
+ };
+ return;
+ }
+ if (!entity.rack) {
+ return;
+ }
+ const parsed = Number(value);
+ if (field === 'unit') {
+ entity.rack.unit = Number.isInteger(parsed) && parsed >= 1 ? parsed : 1;
+ } else if (Number.isInteger(parsed) && parsed >= 1) {
+ entity.rack.heightU = parsed;
+ } else {
+ delete entity.rack.heightU;
+ }
+ },
+ { autosave: true }
+ );
+ }
+
function updatePortCableField(
kind: OwnerKind,
ownerName: string,
@@ -1841,6 +1889,20 @@
+ {#if diagramViewMode === 'rack'}
+
{
+ const { kind, name } = event.detail;
+ const index =
+ kind === 'machine' ? findMachineIndexByName(name) : findDeviceIndexByName(name);
+ if (index >= 0) {
+ selectedTarget = { type: kind, index };
+ }
+ }}
+ />
+ {/if}
+
+
+
+
Virtual Machines
@@ -2676,6 +2806,61 @@
+
+
Ports