Replace mermaid.js with cytoscape.js for network diagram

This commit is contained in:
Ian Gulliver
2026-01-24 14:26:38 -08:00
parent 7c0d4ad05a
commit fdd60a39e1
3 changed files with 179 additions and 2108 deletions

32
static/cytoscape.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -3,116 +3,184 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tendrils Network Diagram</title> <title>Tendrils Network</title>
<style> <style>
* { box-sizing: border-box; }
body { body {
font-family: system-ui, -apple-system, sans-serif; font-family: system-ui, sans-serif;
margin: 0; margin: 0;
padding: 20px; padding: 10px;
background: #1a1a2e; background: #111;
color: #eee; color: #eee;
height: 100vh;
} }
h1 { #controls {
margin: 0 0 20px 0; margin-bottom: 10px;
font-size: 1.5em;
} }
#diagram { #controls button {
background: #16213e; background: #333;
border-radius: 8px; color: #fff;
padding: 20px; border: 1px solid #555;
overflow: auto; padding: 6px 12px;
margin-right: 5px;
border-radius: 4px;
cursor: pointer;
} }
#error { #controls button:hover { background: #444; }
color: #ff6b6b; #cy {
padding: 20px; background: #1a1a1a;
display: none; border: 1px solid #333;
} height: calc(100vh - 50px);
.mermaid { width: 100%;
display: flex;
justify-content: center;
} }
#error { color: #f66; padding: 20px; }
</style> </style>
</head> </head>
<body> <body>
<h1>Tendrils Network</h1> <div id="controls">
<div id="error"></div> <strong>Tendrils</strong>
<div id="diagram"> <button onclick="doLayout()">Layout</button>
<pre class="mermaid" id="mermaid-content"></pre> <button onclick="cy.fit(50)">Fit</button>
<span id="stats"></span>
</div> </div>
<div id="error"></div>
<div id="cy"></div>
<script src="mermaid.min.js"></script> <script src="cytoscape.min.js"></script>
<script> <script>
mermaid.initialize({ let cy;
startOnLoad: false,
theme: 'dark',
flowchart: {
useMaxWidth: true,
htmlLabels: true,
curve: 'basis'
}
});
function sanitizeId(str) { function getLabel(node) {
return str.replace(/[^a-zA-Z0-9]/g, '_'); if (node.names && node.names.length > 0) return node.names[0];
return '??';
} }
function getNodeLabel(node) { function isSwitch(node) {
if (node.names && node.names.length > 0) { return !!(node.poe_budget);
return node.names[0];
}
return node.typeid.substring(0, 12);
} }
async function fetchAndRender() { function doLayout() {
try { cy.layout({
const response = await fetch('/api/status'); name: 'cose',
if (!response.ok) { animate: false,
throw new Error(`HTTP ${response.status}`); padding: 50,
} nodeRepulsion: 10000,
const data = await response.json(); idealEdgeLength: 120,
gravity: 0.2,
numIter: 1000,
fit: true
}).run();
}
const nodes = data.nodes || []; async function init() {
const links = data.links || []; const resp = await fetch('/api/status');
const data = await resp.json();
if (nodes.length === 0) { const nodes = data.nodes || [];
document.getElementById('mermaid-content').textContent = 'No nodes found'; const links = data.links || [];
return;
}
let diagram = 'graph TD\n'; document.getElementById('stats').textContent =
`${nodes.length} nodes, ${links.length} links`;
const nodeIds = new Map(); const elements = [];
nodes.forEach((node, i) => { const idMap = new Map();
const id = 'N' + i; const switchIds = new Set();
nodeIds.set(node.typeid, id);
const label = getNodeLabel(node);
diagram += ` ${id}["${label}"]\n`;
});
links.forEach(link => { nodes.forEach((n, i) => {
const idA = nodeIds.get(link.node_a?.typeid); const id = 'n' + i;
const idB = nodeIds.get(link.node_b?.typeid); idMap.set(n.typeid, id);
if (idA && idB) { if (isSwitch(n)) switchIds.add(id);
if (link.interface_a && link.interface_b) { });
diagram += ` ${idA} ---|${link.interface_a} - ${link.interface_b}| ${idB}\n`;
} else { nodes.forEach((n, i) => {
diagram += ` ${idA} --- ${idB}\n`; const id = 'n' + i;
} const sw = switchIds.has(id);
elements.push({
data: {
id: id,
label: getLabel(n),
isSwitch: sw
} }
}); });
});
document.getElementById('mermaid-content').textContent = diagram; links.forEach((link, i) => {
await mermaid.run({ const idA = idMap.get(link.node_a?.typeid);
nodes: [document.getElementById('mermaid-content')] const idB = idMap.get(link.node_b?.typeid);
if (!idA || !idB) return;
let label = '';
if (link.interface_a) label = link.interface_a;
if (link.interface_b) label += (label ? ' ↔ ' : '') + link.interface_b;
elements.push({
data: {
id: 'e' + i,
source: idA,
target: idB,
label: label
}
}); });
});
} catch (err) { cy = cytoscape({
document.getElementById('error').style.display = 'block'; container: document.getElementById('cy'),
document.getElementById('error').textContent = 'Error loading network data: ' + err.message; elements: elements,
} style: [
{
selector: 'node',
style: {
'label': 'data(label)',
'text-valign': 'center',
'text-halign': 'center',
'background-color': '#a6d',
'color': '#fff',
'font-size': 12,
'width': 120,
'height': 40,
'padding': 8,
'shape': 'round-rectangle',
'text-wrap': 'wrap',
'text-max-width': 110
}
},
{
selector: 'node[?isSwitch]',
style: {
'background-color': '#2a2',
'border-width': 3,
'border-color': '#4f4',
'font-size': 14,
'font-weight': 'bold',
'width': 100,
'height': 50
}
},
{
selector: 'edge',
style: {
'width': 2,
'line-color': '#666',
'curve-style': 'bezier',
'label': 'data(label)',
'font-size': 9,
'color': '#aaa',
'text-background-color': '#1a1a1a',
'text-background-opacity': 1,
'text-background-padding': 2,
'text-rotation': 'autorotate'
}
}
],
layout: { name: 'preset' }
});
doLayout();
} }
fetchAndRender(); init().catch(e => {
document.getElementById('error').textContent = e.message;
});
</script> </script>
</body> </body>
</html> </html>

2029
static/mermaid.min.js vendored

File diff suppressed because one or more lines are too long