From dd8e1b241834844e02a4c0e64053a3b472766d9f Mon Sep 17 00:00:00 2001 From: mpuckett Date: Thu, 14 May 2026 18:17:17 -0500 Subject: [PATCH] Added fields for port mappings --- README.md | 10 ++- src/App.tsx | 202 +++++++++++++++++++++++++++++++++++++++++---- src/types.ts | 16 +++- src/utils/model.ts | 55 +++++++++++- vite.config.ts | 4 + 5 files changed, 266 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 429eb05..e028d51 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ Datacenter Modeler is a React-based web application for designing datacenter rack and cabinet layouts with drag-and-drop equipment placement. Users can build rack diagrams, edit component metadata, export reports, and save/load layouts as JSON. +# Written by Matthew Puckett +# https://www.theblindengineer.com + ## Features - Drag racks and cabinets onto the workspace @@ -30,7 +33,10 @@ Datacenter Modeler is a React-based web application for designing datacenter rac - Edit patch panel-specific properties: - Copper or fiber optic medium - Number of ports - - Port identification + - Dynamic port map with Port ID and Cable ID fields +- Edit component port-map properties for Switch, Firewall, Storage Switch, Router, Server, Blade Chassis, Storage Array, and KVM: + - Dynamic port map with Port ID, Cable ID, and VLAN fields + - VLAN defaults to 1 for new component ports - Edit PDU-specific properties: - Horizontal rack mount or vertical side mount - Horizontal size from 1U to 12U @@ -143,7 +149,7 @@ Diagram data is stored as JSON with this top-level shape: } ``` -Each rack/cabinet contains its own metadata and an array of installed equipment. Equipment records include type, name, size, U position, manufacturer/model details, asset information, power, notes, and display color. Patch panel records also include medium, port count, and port identification metadata. PDU records include mounting orientation, side placement for vertical PDUs, input voltage, output port definitions, and managed-device connection metadata. +Each rack/cabinet contains its own metadata and an array of installed equipment. Equipment records include type, name, size, U position, manufacturer/model details, asset information, power, notes, and display color. Patch panel and network-capable component records also include structured port map metadata. PDU records include mounting orientation, side placement for vertical PDUs, input voltage, output port definitions, and managed-device connection metadata. ## Project Structure diff --git a/src/App.tsx b/src/App.tsx index d77016a..d8a3741 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -74,6 +74,16 @@ import { const STORAGE_KEY = 'datacenter-modeler.diagram'; const RACK_ROW_HEIGHT = 14; +const PORT_MAPPED_COMPONENTS = new Set([ + 'blade-chassis', + 'firewall', + 'kvm', + 'router', + 'server-rack', + 'storage-array', + 'storage-switch', + 'switch', +]); const icons: Record = { rack: , @@ -523,8 +533,9 @@ export default function App() { .forEach((item) => { const patchPanelDetails = item.type === 'patch-panel' - ? ` | ${item.patchPanelMedium === 'fiberoptic' ? 'Fiber optic' : 'Copper'} | ${item.patchPanelPorts} ports` + ? ` | ${item.patchPanelMedium === 'fiberoptic' ? 'Fiber optic' : 'Copper'} | ${item.patchPanelPorts} ports | ${item.patchPanelPortMap.length} mapped` : ''; + const networkPortDetails = PORT_MAPPED_COMPONENTS.has(item.type) ? ` | ${item.networkPorts.length} mapped ports` : ''; const pduDetails = item.type === 'pdu' ? ` | ${item.pduMount === 'vertical' ? `Vertical ${item.pduSide}, ${item.sizeU}U wide` : `Horizontal ${item.sizeU}U`} | In ${item.pduInputVoltage || 'n/a'} | ${item.pduOutputPorts.length} outputs${item.pduManaged ? ` | Managed ${item.pduHostname || 'no host'}` : ''}` @@ -533,7 +544,7 @@ export default function App() { item.type === 'pdu' && item.pduMount === 'vertical' ? `${item.pduSide === 'left' ? 'Left' : 'Right'} side` : `U${item.uStart}-${item.uStart + item.sizeU - 1}`; - const line = `${rackPosition}: ${item.name} | ${item.manufacturer || 'n/a'} ${item.model || ''} | ${item.assetTag || 'no asset tag'}${patchPanelDetails}${pduDetails}`; + const line = `${rackPosition}: ${item.name} | ${item.manufacturer || 'n/a'} ${item.model || ''} | ${item.assetTag || 'no asset tag'}${patchPanelDetails}${networkPortDetails}${pduDetails}`; pdf.text(line.slice(0, 115), margin + 12, y); y += 14; }); @@ -1256,19 +1267,180 @@ function PropertiesPanel({ } fullWidth /> - - onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, { - patchPanelPortIdentification: event.target.value, - }) - } - multiline - minRows={3} - placeholder="A01-A24, VLAN labels, fiber strands, or room/circuit references" - fullWidth - /> + + + + Port Map + + + + {selectedEquipment.patchPanelPortMap.length === 0 ? ( + + No ports mapped. + + ) : ( + selectedEquipment.patchPanelPortMap.map((port) => ( + + + onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, { + patchPanelPortMap: selectedEquipment.patchPanelPortMap.map((item) => + item.id === port.id ? { ...item, portId: event.target.value } : item, + ), + }) + } + sx={{ flex: 1 }} + /> + + onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, { + patchPanelPortMap: selectedEquipment.patchPanelPortMap.map((item) => + item.id === port.id ? { ...item, cableId: event.target.value } : item, + ), + }) + } + sx={{ flex: 1 }} + /> + + + onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, { + patchPanelPortMap: selectedEquipment.patchPanelPortMap.filter((item) => item.id !== port.id), + }) + } + > + + + + + )) + )} + + + )} + {PORT_MAPPED_COMPONENTS.has(selectedEquipment.type) && ( + <> + + + Component Ports + + + + + Port Map + + + + {selectedEquipment.networkPorts.length === 0 ? ( + + No component ports mapped. + + ) : ( + selectedEquipment.networkPorts.map((port) => ( + + + onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, { + networkPorts: selectedEquipment.networkPorts.map((item) => + item.id === port.id ? { ...item, portId: event.target.value } : item, + ), + }) + } + sx={{ flex: 1 }} + /> + + onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, { + networkPorts: selectedEquipment.networkPorts.map((item) => + item.id === port.id ? { ...item, cableId: event.target.value } : item, + ), + }) + } + sx={{ flex: 1 }} + /> + + onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, { + networkPorts: selectedEquipment.networkPorts.map((item) => + item.id === port.id ? { ...item, vlan: clampNumber(event.target.value, 1, 4094, port.vlan) } : item, + ), + }) + } + sx={{ flex: 0.8 }} + /> + + + onEquipmentChange(selectedEquipmentRack.id, selectedEquipment.id, { + networkPorts: selectedEquipment.networkPorts.filter((item) => item.id !== port.id), + }) + } + > + + + + + )) + )} + )} {selectedEquipment.type === 'pdu' && ( diff --git a/src/types.ts b/src/types.ts index c77269d..9cf4fb1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,6 +20,19 @@ export type LibraryCategory = 'container' | 'equipment'; export type PatchPanelMedium = '' | 'copper' | 'fiberoptic'; +export interface PatchPanelPort { + id: string; + portId: string; + cableId: string; +} + +export interface NetworkPort { + id: string; + portId: string; + cableId: string; + vlan: number; +} + export type PduMount = 'horizontal' | 'vertical'; export type PduSide = 'left' | 'right'; @@ -54,7 +67,8 @@ export interface Equipment { powerWatts: number; patchPanelMedium: PatchPanelMedium; patchPanelPorts: number; - patchPanelPortIdentification: string; + patchPanelPortMap: PatchPanelPort[]; + networkPorts: NetworkPort[]; pduMount: PduMount; pduSide: PduSide; pduInputVoltage: string; diff --git a/src/utils/model.ts b/src/utils/model.ts index 7ceba60..e0b1c6b 100644 --- a/src/utils/model.ts +++ b/src/utils/model.ts @@ -1,4 +1,4 @@ -import type { DiagramData, Equipment, PduOutputPort, RackContainer } from '../types'; +import type { DiagramData, Equipment, NetworkPort, PatchPanelPort, PduOutputPort, RackContainer } from '../types'; import { getLibraryItem } from '../data/componentLibrary'; export const MAX_CONTAINERS = 10; @@ -55,7 +55,8 @@ export const createEquipment = (type: string, rack: RackContainer, preferredU: n powerWatts: 0, patchPanelMedium: libraryItem.type === 'patch-panel' ? 'copper' : '', patchPanelPorts: libraryItem.type === 'patch-panel' ? 24 : 0, - patchPanelPortIdentification: '', + patchPanelPortMap: [], + networkPorts: [], pduMount: 'horizontal', pduSide: 'right', pduInputVoltage: '', @@ -155,6 +156,12 @@ export const normalizeDiagram = (value: unknown): DiagramData => { notes: rack.notes || '', equipment: Array.isArray(rack.equipment) ? rack.equipment.map((item, itemIndex) => { + const legacyItem = item as Equipment & { + networkPorts?: unknown; + patchPanelPortIdentification?: unknown; + patchPanelPortMap?: unknown; + switchPorts?: unknown; + }; const libraryItem = getLibraryItem(item.type) || getLibraryItem('server-rack')!; const pduMount = item.pduMount === 'vertical' ? 'vertical' : 'horizontal'; const maxSizeU = libraryItem.type === 'pdu' ? (pduMount === 'vertical' ? 3 : 12) : 24; @@ -177,7 +184,8 @@ export const normalizeDiagram = (value: unknown): DiagramData => { ? 'copper' : '', patchPanelPorts: clampNumber(item.patchPanelPorts, 0, 10000, libraryItem.type === 'patch-panel' ? 24 : 0), - patchPanelPortIdentification: item.patchPanelPortIdentification || '', + patchPanelPortMap: normalizePatchPanelPorts(legacyItem.patchPanelPortMap, legacyItem.patchPanelPortIdentification), + networkPorts: normalizeNetworkPorts(legacyItem.networkPorts ?? legacyItem.switchPorts), pduMount, pduSide: item.pduSide === 'left' ? 'left' : 'right', pduInputVoltage: item.pduInputVoltage || '', @@ -195,6 +203,47 @@ export const normalizeDiagram = (value: unknown): DiagramData => { }; }; +const normalizeNetworkPorts = (ports: unknown): NetworkPort[] => { + if (!Array.isArray(ports)) { + return []; + } + + return ports.map((port, index) => { + const candidate = port as Partial; + return { + id: candidate.id || createId('network-port'), + portId: candidate.portId || `Port ${index + 1}`, + cableId: candidate.cableId || '', + vlan: clampNumber(candidate.vlan, 1, 4094, 1), + }; + }); +}; + +const normalizePatchPanelPorts = (ports: unknown, legacyIdentification: unknown): PatchPanelPort[] => { + if (Array.isArray(ports)) { + return ports.map((port, index) => { + const candidate = port as Partial; + return { + id: candidate.id || createId('patch-port'), + portId: candidate.portId || `Port ${index + 1}`, + cableId: candidate.cableId || '', + }; + }); + } + + if (typeof legacyIdentification === 'string' && legacyIdentification.trim()) { + return [ + { + id: createId('patch-port'), + portId: legacyIdentification, + cableId: '', + }, + ]; + } + + return []; +}; + const normalizePduOutputPorts = (ports: unknown): PduOutputPort[] => { if (!Array.isArray(ports)) { return []; diff --git a/vite.config.ts b/vite.config.ts index 2181a6f..848bf97 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,4 +3,8 @@ import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react], + server: { + host: '0.0.0.0', + allowedHosts: ['datacentermodeler.com', 'www.datacentermodeler.com', 'localhost', '127.0.0.1'] + } });