mirror of
https://github.com/jcreek/OpenNetworkDiagram.git
synced 2026-07-14 03:23:44 +00:00
feat(#2): replace card layout with cytoscape network graph
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
import type { ConnectedPortRef, NetworkData, Port } from '../types';
|
||||
import type { GraphEdgeElement, GraphNodeElement, GraphTransformResult } from './types';
|
||||
|
||||
interface EndpointOwner {
|
||||
nodeId: string;
|
||||
name: string;
|
||||
ports: Map<string, Port>;
|
||||
}
|
||||
|
||||
function toMachineNodeId(name: string): string {
|
||||
return `machine:${encodeURIComponent(name)}`;
|
||||
}
|
||||
|
||||
function toDeviceNodeId(name: string): string {
|
||||
return `device:${encodeURIComponent(name)}`;
|
||||
}
|
||||
|
||||
function toVmNodeId(hostName: string, vmName: string): string {
|
||||
return `vm:${encodeURIComponent(hostName)}:${encodeURIComponent(vmName)}`;
|
||||
}
|
||||
|
||||
function parseConnectedTo(connection: Port['connectedTo']): ConnectedPortRef | null {
|
||||
if (!connection?.device || !connection?.port) return null;
|
||||
return {
|
||||
device: connection.device.trim(),
|
||||
port: connection.port.trim()
|
||||
};
|
||||
}
|
||||
|
||||
function hasReciprocalLink(
|
||||
targetPort: Port | undefined,
|
||||
expectedDeviceName: string,
|
||||
expectedPortName: string
|
||||
): boolean {
|
||||
const reverse = parseConnectedTo(targetPort?.connectedTo);
|
||||
if (!reverse) return false;
|
||||
return reverse.device === expectedDeviceName && reverse.port === expectedPortName;
|
||||
}
|
||||
|
||||
function connectionKey(a: string, b: string): string {
|
||||
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
||||
}
|
||||
|
||||
function edgeSpeed(a: number | undefined, b: number | undefined): number | undefined {
|
||||
if (typeof a === 'number' && typeof b === 'number') return Math.min(a, b);
|
||||
return typeof a === 'number' ? a : b;
|
||||
}
|
||||
|
||||
export function transformNetworkDataToGraph(data: NetworkData): GraphTransformResult {
|
||||
const nodes: GraphNodeElement[] = [];
|
||||
const edges: GraphEdgeElement[] = [];
|
||||
const warnings: string[] = [];
|
||||
const machineVmIndex: Record<
|
||||
string,
|
||||
{
|
||||
vmNodeIds: string[];
|
||||
hostingEdgeIds: string[];
|
||||
vmCount: number;
|
||||
}
|
||||
> = {};
|
||||
const ownersByName = new Map<string, EndpointOwner>();
|
||||
const seenWarnings = new Set<string>();
|
||||
|
||||
const warn = (message: string) => {
|
||||
if (!seenWarnings.has(message)) {
|
||||
seenWarnings.add(message);
|
||||
warnings.push(message);
|
||||
}
|
||||
};
|
||||
|
||||
const addOwner = (owner: EndpointOwner) => {
|
||||
if (ownersByName.has(owner.name)) {
|
||||
warn(
|
||||
`Duplicate node name "${owner.name}" found. First occurrence will be used for link resolution.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
ownersByName.set(owner.name, owner);
|
||||
};
|
||||
|
||||
let hostingEdgeCounter = 0;
|
||||
|
||||
for (const machine of data.machines) {
|
||||
const machineNodeId = toMachineNodeId(machine.machineName);
|
||||
const machinePortsList = [...(machine.ports ?? [])];
|
||||
const machineVms = machine.software.vms.map((vm) => ({
|
||||
name: vm.name,
|
||||
ip: vm.ipAddress,
|
||||
role: vm.role
|
||||
}));
|
||||
|
||||
nodes.push({
|
||||
data: {
|
||||
id: machineNodeId,
|
||||
label: machine.machineName,
|
||||
rawName: machine.machineName,
|
||||
kind: 'machine',
|
||||
nodeWidth: 200,
|
||||
nodeHeight: 110,
|
||||
details: {
|
||||
type: 'machine',
|
||||
name: machine.machineName,
|
||||
ip: machine.ipAddress,
|
||||
role: machine.role,
|
||||
os: machine.operatingSystem,
|
||||
cpu: machine.hardware.cpu,
|
||||
ram: machine.hardware.ram,
|
||||
gpu: machine.hardware.gpu,
|
||||
ports: machinePortsList,
|
||||
vmCount: machineVms.length,
|
||||
vms: machineVms
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
machineVmIndex[machineNodeId] = {
|
||||
vmNodeIds: [],
|
||||
hostingEdgeIds: [],
|
||||
vmCount: machine.software.vms.length
|
||||
};
|
||||
|
||||
for (const vm of machine.software.vms) {
|
||||
const vmNodeId = toVmNodeId(machine.machineName, vm.name);
|
||||
|
||||
nodes.push({
|
||||
data: {
|
||||
id: vmNodeId,
|
||||
label: vm.name,
|
||||
rawName: vm.name,
|
||||
kind: 'vm',
|
||||
hostMachineId: machineNodeId,
|
||||
nodeWidth: 140,
|
||||
nodeHeight: 66,
|
||||
details: {
|
||||
type: 'vm',
|
||||
name: vm.name,
|
||||
ip: vm.ipAddress,
|
||||
role: vm.role,
|
||||
hostName: machine.machineName
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
hostingEdgeCounter += 1;
|
||||
const hostingEdgeId = `hosting:${hostingEdgeCounter}`;
|
||||
edges.push({
|
||||
data: {
|
||||
id: hostingEdgeId,
|
||||
source: machineNodeId,
|
||||
target: vmNodeId,
|
||||
kind: 'hosting',
|
||||
label: 'hosts'
|
||||
}
|
||||
});
|
||||
|
||||
machineVmIndex[machineNodeId].vmNodeIds.push(vmNodeId);
|
||||
machineVmIndex[machineNodeId].hostingEdgeIds.push(hostingEdgeId);
|
||||
}
|
||||
|
||||
const machinePorts = new Map<string, Port>();
|
||||
for (const port of machine.ports ?? []) {
|
||||
machinePorts.set(port.portName, port);
|
||||
}
|
||||
|
||||
addOwner({
|
||||
nodeId: machineNodeId,
|
||||
name: machine.machineName,
|
||||
ports: machinePorts
|
||||
});
|
||||
}
|
||||
|
||||
for (const device of data.devices) {
|
||||
const deviceNodeId = toDeviceNodeId(device.name);
|
||||
const devicePorts = [...(device.ports ?? [])];
|
||||
|
||||
nodes.push({
|
||||
data: {
|
||||
id: deviceNodeId,
|
||||
label: device.name,
|
||||
rawName: device.name,
|
||||
kind: 'device',
|
||||
nodeWidth: 176,
|
||||
nodeHeight: 116,
|
||||
details: {
|
||||
type: 'device',
|
||||
name: device.name,
|
||||
ip: device.ipAddress,
|
||||
deviceType: device.type,
|
||||
notes: device.notes,
|
||||
ports: devicePorts
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const devicePortMap = new Map<string, Port>();
|
||||
for (const port of device.ports ?? []) {
|
||||
devicePortMap.set(port.portName, port);
|
||||
}
|
||||
|
||||
addOwner({
|
||||
nodeId: deviceNodeId,
|
||||
name: device.name,
|
||||
ports: devicePortMap
|
||||
});
|
||||
}
|
||||
|
||||
const seenPhysicalConnections = new Set<string>();
|
||||
let physicalEdgeCounter = 0;
|
||||
|
||||
for (const owner of ownersByName.values()) {
|
||||
for (const port of owner.ports.values()) {
|
||||
const connectedTo = parseConnectedTo(port.connectedTo);
|
||||
if (!connectedTo) continue;
|
||||
|
||||
const targetOwner = ownersByName.get(connectedTo.device);
|
||||
if (!targetOwner) {
|
||||
warn(
|
||||
`Link from "${owner.name}:${port.portName}" references unknown device "${connectedTo.device}".`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetPort = targetOwner.ports.get(connectedTo.port);
|
||||
if (!targetPort) {
|
||||
warn(
|
||||
`Link from "${owner.name}:${port.portName}" references unknown port "${connectedTo.device}:${connectedTo.port}".`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const a = `${owner.nodeId}:${port.portName}`;
|
||||
const b = `${targetOwner.nodeId}:${connectedTo.port}`;
|
||||
const dedupeKey = connectionKey(a, b);
|
||||
if (seenPhysicalConnections.has(dedupeKey)) continue;
|
||||
seenPhysicalConnections.add(dedupeKey);
|
||||
|
||||
const reciprocal = hasReciprocalLink(targetPort, owner.name, port.portName);
|
||||
if (!reciprocal) {
|
||||
warn(
|
||||
`Non-reciprocal link: "${owner.name}:${port.portName}" points to "${connectedTo.device}:${connectedTo.port}" but reverse link is missing.`
|
||||
);
|
||||
}
|
||||
|
||||
physicalEdgeCounter += 1;
|
||||
const speedGbps = edgeSpeed(port.speedGbps, targetPort.speedGbps);
|
||||
|
||||
edges.push({
|
||||
data: {
|
||||
id: `physical:${physicalEdgeCounter}`,
|
||||
source: owner.nodeId,
|
||||
target: targetOwner.nodeId,
|
||||
kind: 'physical',
|
||||
label: speedGbps ? `${speedGbps}GbE` : undefined,
|
||||
sourcePort: port.portName,
|
||||
targetPort: connectedTo.port,
|
||||
speedGbps,
|
||||
reciprocal
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes, edges, warnings, machineVmIndex };
|
||||
}
|
||||
Reference in New Issue
Block a user