diff --git a/src/lib/data/networkEditor.ts b/src/lib/data/networkEditor.ts new file mode 100644 index 0000000..7f81eab --- /dev/null +++ b/src/lib/data/networkEditor.ts @@ -0,0 +1,359 @@ +import type { Machine, NetworkData, NetworkDevice, Port, VM } from '../types'; + +export type OwnerKind = 'machine' | 'device'; + +interface OwnerRef { + kind: OwnerKind; + name: string; + ports: Port[]; +} + +function deepClone(value: T): T { + if (typeof structuredClone === 'function') { + return structuredClone(value); + } + return JSON.parse(JSON.stringify(value)) as T; +} + +function equalsIgnoreCase(a: string, b: string): boolean { + return a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0; +} + +function normalizePorts(ports: Port[] | undefined): Port[] { + return Array.isArray(ports) ? ports : []; +} + +function normalizeVms(vms: VM[] | undefined): VM[] { + return Array.isArray(vms) ? vms : []; +} + +function allOwners(data: NetworkData): OwnerRef[] { + const machineOwners = data.machines.map((machine) => { + const ports = normalizePorts(machine.ports); + return { + kind: 'machine' as const, + name: machine.machineName, + ports + }; + }); + const deviceOwners = data.devices.map((device) => { + const ports = normalizePorts(device.ports); + return { + kind: 'device' as const, + name: device.name, + ports + }; + }); + return [...machineOwners, ...deviceOwners]; +} + +export function cloneNetworkData(data: NetworkData): NetworkData { + const cloned = deepClone(data); + for (const machine of cloned.machines) { + machine.ports = normalizePorts(machine.ports); + machine.software = { + vms: normalizeVms(machine.software?.vms) + }; + } + for (const device of cloned.devices) { + device.ports = normalizePorts(device.ports); + } + return cloned; +} + +function findOwnerByKindAndName(data: NetworkData, kind: OwnerKind, name: string): OwnerRef | undefined { + if (kind === 'machine') { + const machine = data.machines.find((item) => equalsIgnoreCase(item.machineName, name)); + if (!machine) { + return undefined; + } + machine.ports = normalizePorts(machine.ports); + return { + kind, + name: machine.machineName, + ports: machine.ports + }; + } + + const device = data.devices.find((item) => equalsIgnoreCase(item.name, name)); + if (!device) { + return undefined; + } + device.ports = normalizePorts(device.ports); + return { + kind, + name: device.name, + ports: device.ports + }; +} + +function findOwnerByName(data: NetworkData, name: string): OwnerRef | undefined { + const owners = allOwners(data); + return owners.find((owner) => equalsIgnoreCase(owner.name, name)); +} + +function findPort(owner: OwnerRef, portName: string): Port | undefined { + return owner.ports.find((port) => equalsIgnoreCase(port.portName, portName)); +} + +function ensureReciprocalConnections(data: NetworkData) { + const owners = allOwners(data); + + for (const owner of owners) { + for (const port of owner.ports) { + if (!port.connectedTo) { + continue; + } + + const targetOwner = owners.find((candidate) => + equalsIgnoreCase(candidate.name, port.connectedTo!.device) + ); + if (!targetOwner) { + delete port.connectedTo; + continue; + } + + const targetPort = findPort(targetOwner, port.connectedTo.port); + if (!targetPort) { + delete port.connectedTo; + continue; + } + + targetPort.connectedTo = { + device: owner.name, + port: port.portName + }; + } + } +} + +function clearInboundReferences(data: NetworkData, predicate: (connection: Port['connectedTo']) => boolean) { + for (const owner of allOwners(data)) { + for (const port of owner.ports) { + if (!port.connectedTo) { + continue; + } + if (predicate(port.connectedTo)) { + delete port.connectedTo; + } + } + } +} + +export function withReconciledConnections(data: NetworkData): NetworkData { + const cloned = cloneNetworkData(data); + ensureReciprocalConnections(cloned); + return cloned; +} + +export function createEmptyMachine(): Machine { + return { + machineName: 'New Machine', + ipAddress: '192.168.1.100', + role: 'Role', + operatingSystem: 'OS', + software: { + vms: [] + }, + hardware: { + cpu: 'CPU', + ram: '8GB', + networkPorts: 1 + }, + ports: [] + }; +} + +export function createEmptyDevice(): NetworkDevice { + return { + name: 'New Device', + ipAddress: '192.168.1.200', + type: 'Device', + notes: '', + ports: [] + }; +} + +export function createEmptyVm(): VM { + return { + name: 'New VM', + role: 'Role', + ipAddress: '192.168.1.201' + }; +} + +export function createEmptyPort(prefix = 'eth'): Port { + return { + portName: `${prefix}0`, + speedGbps: 1 + }; +} + +export function renameOwner( + data: NetworkData, + kind: OwnerKind, + previousName: string, + nextName: string +): NetworkData { + const cloned = cloneNetworkData(data); + if (equalsIgnoreCase(previousName, nextName)) { + return cloned; + } + + if (kind === 'machine') { + const machine = cloned.machines.find((item) => equalsIgnoreCase(item.machineName, previousName)); + if (machine) { + machine.machineName = nextName; + } + } else { + const device = cloned.devices.find((item) => equalsIgnoreCase(item.name, previousName)); + if (device) { + device.name = nextName; + } + } + + for (const owner of allOwners(cloned)) { + for (const port of owner.ports) { + if (port.connectedTo && equalsIgnoreCase(port.connectedTo.device, previousName)) { + port.connectedTo.device = nextName; + } + } + } + + ensureReciprocalConnections(cloned); + return cloned; +} + +export function renamePort( + data: NetworkData, + kind: OwnerKind, + ownerName: string, + previousPortName: string, + nextPortName: string +): NetworkData { + const cloned = cloneNetworkData(data); + if (equalsIgnoreCase(previousPortName, nextPortName)) { + return cloned; + } + + const owner = findOwnerByKindAndName(cloned, kind, ownerName); + if (!owner) { + return cloned; + } + + const port = findPort(owner, previousPortName); + if (!port) { + return cloned; + } + port.portName = nextPortName; + + for (const candidateOwner of allOwners(cloned)) { + for (const candidatePort of candidateOwner.ports) { + if ( + candidatePort.connectedTo && + equalsIgnoreCase(candidatePort.connectedTo.device, owner.name) && + equalsIgnoreCase(candidatePort.connectedTo.port, previousPortName) + ) { + candidatePort.connectedTo.port = nextPortName; + } + } + } + + ensureReciprocalConnections(cloned); + return cloned; +} + +export function deleteOwner(data: NetworkData, kind: OwnerKind, ownerName: string): NetworkData { + const cloned = cloneNetworkData(data); + + if (kind === 'machine') { + cloned.machines = cloned.machines.filter((machine) => !equalsIgnoreCase(machine.machineName, ownerName)); + } else { + cloned.devices = cloned.devices.filter((device) => !equalsIgnoreCase(device.name, ownerName)); + } + + clearInboundReferences( + cloned, + (connection) => Boolean(connection && equalsIgnoreCase(connection.device, ownerName)) + ); + ensureReciprocalConnections(cloned); + return cloned; +} + +export function deletePort( + data: NetworkData, + kind: OwnerKind, + ownerName: string, + portName: string +): NetworkData { + const cloned = cloneNetworkData(data); + const owner = findOwnerByKindAndName(cloned, kind, ownerName); + if (!owner) { + return cloned; + } + + owner.ports = owner.ports.filter((port) => !equalsIgnoreCase(port.portName, portName)); + + if (kind === 'machine') { + const machine = cloned.machines.find((item) => equalsIgnoreCase(item.machineName, ownerName)); + if (machine) { + machine.ports = owner.ports; + } + } else { + const device = cloned.devices.find((item) => equalsIgnoreCase(item.name, ownerName)); + if (device) { + device.ports = owner.ports; + } + } + + clearInboundReferences( + cloned, + (connection) => + Boolean( + connection && + equalsIgnoreCase(connection.device, ownerName) && + equalsIgnoreCase(connection.port, portName) + ) + ); + ensureReciprocalConnections(cloned); + return cloned; +} + +export function setPortConnection( + data: NetworkData, + kind: OwnerKind, + ownerName: string, + portName: string, + target: Port['connectedTo'] | undefined +): NetworkData { + const cloned = cloneNetworkData(data); + const owner = findOwnerByKindAndName(cloned, kind, ownerName); + if (!owner) { + return cloned; + } + const port = findPort(owner, portName); + if (!port) { + return cloned; + } + + const previousConnection = port.connectedTo; + if (previousConnection) { + const previousOwner = findOwnerByName(cloned, previousConnection.device); + const previousPort = previousOwner ? findPort(previousOwner, previousConnection.port) : undefined; + if (previousPort) { + delete previousPort.connectedTo; + } + } + + if (!target?.device || !target.port) { + delete port.connectedTo; + return cloned; + } + + port.connectedTo = { + device: target.device, + port: target.port + }; + ensureReciprocalConnections(cloned); + return cloned; +}