Replace mermaid.js with cytoscape.js for network diagram
This commit is contained in:
32
static/cytoscape.min.js
vendored
Normal file
32
static/cytoscape.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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
2029
static/mermaid.min.js
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user