Add switch uplink display with spanning tree topology
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,10 +13,6 @@
|
|||||||
background: #111;
|
background: #111;
|
||||||
color: #eee;
|
color: #eee;
|
||||||
}
|
}
|
||||||
#controls {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
#stats { margin-left: 10px; }
|
|
||||||
#error { color: #f66; padding: 20px; }
|
#error { color: #f66; padding: 20px; }
|
||||||
|
|
||||||
#container {
|
#container {
|
||||||
@@ -103,6 +99,19 @@
|
|||||||
color: #f99;
|
color: #f99;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node .uplink {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 9px;
|
||||||
|
background: #446;
|
||||||
|
color: #aaf;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.node:hover {
|
.node:hover {
|
||||||
filter: brightness(1.2);
|
filter: brightness(1.2);
|
||||||
}
|
}
|
||||||
@@ -138,10 +147,6 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="controls">
|
|
||||||
<strong>Tendrils</strong>
|
|
||||||
<span id="stats"></span>
|
|
||||||
</div>
|
|
||||||
<div id="error"></div>
|
<div id="error"></div>
|
||||||
<div id="container"></div>
|
<div id="container"></div>
|
||||||
|
|
||||||
@@ -252,7 +257,7 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNodeElement(node, switchConnection, nodeLocation) {
|
function createNodeElement(node, switchConnection, nodeLocation, uplinkInfo) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'node' + (isSwitch(node) ? ' switch' : '');
|
div.className = 'node' + (isSwitch(node) ? ' switch' : '');
|
||||||
|
|
||||||
@@ -272,6 +277,13 @@
|
|||||||
labelEl.textContent = getLabel(node);
|
labelEl.textContent = getLabel(node);
|
||||||
div.appendChild(labelEl);
|
div.appendChild(labelEl);
|
||||||
|
|
||||||
|
if (isSwitch(node) && uplinkInfo) {
|
||||||
|
const uplinkEl = document.createElement('div');
|
||||||
|
uplinkEl.className = 'uplink';
|
||||||
|
uplinkEl.textContent = uplinkInfo.localPort + ' → ' + uplinkInfo.parentName + ':' + uplinkInfo.remotePort;
|
||||||
|
div.appendChild(uplinkEl);
|
||||||
|
}
|
||||||
|
|
||||||
div.addEventListener('click', () => {
|
div.addEventListener('click', () => {
|
||||||
const json = JSON.stringify(node, null, 2);
|
const json = JSON.stringify(node, null, 2);
|
||||||
navigator.clipboard.writeText(json).then(() => {
|
navigator.clipboard.writeText(json).then(() => {
|
||||||
@@ -282,12 +294,12 @@
|
|||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLocation(loc, assignedNodes, isTopLevel, switchConnections) {
|
function renderLocation(loc, assignedNodes, isTopLevel, switchConnections, switchUplinks) {
|
||||||
const nodes = assignedNodes.get(loc) || [];
|
const nodes = assignedNodes.get(loc) || [];
|
||||||
const hasNodes = nodes.length > 0;
|
const hasNodes = nodes.length > 0;
|
||||||
|
|
||||||
const childElements = loc.children
|
const childElements = loc.children
|
||||||
.map(child => renderLocation(child, assignedNodes, false, switchConnections))
|
.map(child => renderLocation(child, assignedNodes, false, switchConnections, switchUplinks))
|
||||||
.filter(el => el !== null);
|
.filter(el => el !== null);
|
||||||
|
|
||||||
if (!hasNodes && childElements.length === 0) {
|
if (!hasNodes && childElements.length === 0) {
|
||||||
@@ -313,7 +325,8 @@
|
|||||||
const switchRow = document.createElement('div');
|
const switchRow = document.createElement('div');
|
||||||
switchRow.className = 'node-row';
|
switchRow.className = 'node-row';
|
||||||
switches.forEach(node => {
|
switches.forEach(node => {
|
||||||
switchRow.appendChild(createNodeElement(node, null, loc));
|
const uplink = switchUplinks.get(node.typeid);
|
||||||
|
switchRow.appendChild(createNodeElement(node, null, loc, uplink));
|
||||||
});
|
});
|
||||||
container.appendChild(switchRow);
|
container.appendChild(switchRow);
|
||||||
}
|
}
|
||||||
@@ -323,7 +336,7 @@
|
|||||||
nodeRow.className = 'node-row';
|
nodeRow.className = 'node-row';
|
||||||
nonSwitches.forEach(node => {
|
nonSwitches.forEach(node => {
|
||||||
const conn = switchConnections.get(node.typeid);
|
const conn = switchConnections.get(node.typeid);
|
||||||
nodeRow.appendChild(createNodeElement(node, conn, loc));
|
nodeRow.appendChild(createNodeElement(node, conn, loc, null));
|
||||||
});
|
});
|
||||||
container.appendChild(nodeRow);
|
container.appendChild(nodeRow);
|
||||||
}
|
}
|
||||||
@@ -351,8 +364,6 @@
|
|||||||
const nodes = data.nodes || [];
|
const nodes = data.nodes || [];
|
||||||
const links = data.links || [];
|
const links = data.links || [];
|
||||||
|
|
||||||
document.getElementById('stats').textContent =
|
|
||||||
`${nodes.length} nodes, ${links.length} links`;
|
|
||||||
|
|
||||||
const locationTree = buildLocationTree(config.locations || [], null);
|
const locationTree = buildLocationTree(config.locations || [], null);
|
||||||
const nodeIndex = new Map();
|
const nodeIndex = new Map();
|
||||||
@@ -381,6 +392,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const switchConnections = new Map();
|
const switchConnections = new Map();
|
||||||
|
const switchLinks = [];
|
||||||
|
const allSwitches = nodes.filter(n => isSwitch(n));
|
||||||
|
|
||||||
links.forEach(link => {
|
links.forEach(link => {
|
||||||
const nodeA = nodesByTypeId.get(link.node_a?.typeid);
|
const nodeA = nodesByTypeId.get(link.node_a?.typeid);
|
||||||
const nodeB = nodesByTypeId.get(link.node_b?.typeid);
|
const nodeB = nodesByTypeId.get(link.node_b?.typeid);
|
||||||
@@ -389,7 +403,14 @@
|
|||||||
const aIsSwitch = isSwitch(nodeA);
|
const aIsSwitch = isSwitch(nodeA);
|
||||||
const bIsSwitch = isSwitch(nodeB);
|
const bIsSwitch = isSwitch(nodeB);
|
||||||
|
|
||||||
if (aIsSwitch && !bIsSwitch) {
|
if (aIsSwitch && bIsSwitch) {
|
||||||
|
switchLinks.push({
|
||||||
|
switchA: nodeA,
|
||||||
|
switchB: nodeB,
|
||||||
|
portA: link.interface_a || '?',
|
||||||
|
portB: link.interface_b || '?'
|
||||||
|
});
|
||||||
|
} else if (aIsSwitch && !bIsSwitch) {
|
||||||
const nodeLoc = nodeLocations.get(nodeB.typeid);
|
const nodeLoc = nodeLocations.get(nodeB.typeid);
|
||||||
const effectiveSwitch = findEffectiveSwitch(nodeLoc, assignedNodes);
|
const effectiveSwitch = findEffectiveSwitch(nodeLoc, assignedNodes);
|
||||||
switchConnections.set(nodeB.typeid, {
|
switchConnections.set(nodeB.typeid, {
|
||||||
@@ -408,11 +429,73 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const switchUplinks = new Map();
|
||||||
|
if (allSwitches.length > 0 && switchLinks.length > 0) {
|
||||||
|
const adjacency = new Map();
|
||||||
|
allSwitches.forEach(sw => adjacency.set(sw.typeid, []));
|
||||||
|
|
||||||
|
switchLinks.forEach(link => {
|
||||||
|
adjacency.get(link.switchA.typeid).push({
|
||||||
|
neighbor: link.switchB,
|
||||||
|
localPort: link.portA,
|
||||||
|
remotePort: link.portB
|
||||||
|
});
|
||||||
|
adjacency.get(link.switchB.typeid).push({
|
||||||
|
neighbor: link.switchA,
|
||||||
|
localPort: link.portB,
|
||||||
|
remotePort: link.portA
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let bestRoot = allSwitches[0];
|
||||||
|
let bestMaxDepth = Infinity;
|
||||||
|
|
||||||
|
allSwitches.forEach(candidate => {
|
||||||
|
const visited = new Set([candidate.typeid]);
|
||||||
|
const queue = [{ sw: candidate, depth: 0 }];
|
||||||
|
let maxDepth = 0;
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const { sw, depth } = queue.shift();
|
||||||
|
maxDepth = Math.max(maxDepth, depth);
|
||||||
|
for (const edge of adjacency.get(sw.typeid) || []) {
|
||||||
|
if (!visited.has(edge.neighbor.typeid)) {
|
||||||
|
visited.add(edge.neighbor.typeid);
|
||||||
|
queue.push({ sw: edge.neighbor, depth: depth + 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxDepth < bestMaxDepth) {
|
||||||
|
bestMaxDepth = maxDepth;
|
||||||
|
bestRoot = candidate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const visited = new Set([bestRoot.typeid]);
|
||||||
|
const queue = [bestRoot];
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift();
|
||||||
|
for (const edge of adjacency.get(current.typeid) || []) {
|
||||||
|
if (!visited.has(edge.neighbor.typeid)) {
|
||||||
|
visited.add(edge.neighbor.typeid);
|
||||||
|
switchUplinks.set(edge.neighbor.typeid, {
|
||||||
|
localPort: edge.localPort,
|
||||||
|
remotePort: edge.remotePort,
|
||||||
|
parentName: getLabel(current)
|
||||||
|
});
|
||||||
|
queue.push(edge.neighbor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const container = document.getElementById('container');
|
const container = document.getElementById('container');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
locationTree.forEach(loc => {
|
locationTree.forEach(loc => {
|
||||||
const el = renderLocation(loc, assignedNodes, true, switchConnections);
|
const el = renderLocation(loc, assignedNodes, true, switchConnections, switchUplinks);
|
||||||
if (el) container.appendChild(el);
|
if (el) container.appendChild(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -432,7 +515,8 @@
|
|||||||
const switchRow = document.createElement('div');
|
const switchRow = document.createElement('div');
|
||||||
switchRow.className = 'node-row';
|
switchRow.className = 'node-row';
|
||||||
switches.forEach(node => {
|
switches.forEach(node => {
|
||||||
switchRow.appendChild(createNodeElement(node, null, null));
|
const uplink = switchUplinks.get(node.typeid);
|
||||||
|
switchRow.appendChild(createNodeElement(node, null, null, uplink));
|
||||||
});
|
});
|
||||||
unassignedLoc.appendChild(switchRow);
|
unassignedLoc.appendChild(switchRow);
|
||||||
}
|
}
|
||||||
@@ -442,7 +526,7 @@
|
|||||||
nodeRow.className = 'node-row';
|
nodeRow.className = 'node-row';
|
||||||
nonSwitches.forEach(node => {
|
nonSwitches.forEach(node => {
|
||||||
const conn = switchConnections.get(node.typeid);
|
const conn = switchConnections.get(node.typeid);
|
||||||
nodeRow.appendChild(createNodeElement(node, conn, null));
|
nodeRow.appendChild(createNodeElement(node, conn, null, null));
|
||||||
});
|
});
|
||||||
unassignedLoc.appendChild(nodeRow);
|
unassignedLoc.appendChild(nodeRow);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user