153 lines
4.6 KiB
Plaintext
153 lines
4.6 KiB
Plaintext
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Node Balancer</title>
|
|
<style>
|
|
body { font-family: sans-serif; margin: 20px; }
|
|
svg { border:1px solid #ccc; width:100%; height:500px; background:#f9f9f9; }
|
|
.node { fill:#4CAF50; stroke:#333; stroke-width:2; }
|
|
.pod { fill:#2196F3; stroke:#000; stroke-width:1; }
|
|
.label { font-size:14px; text-anchor:middle; }
|
|
.pod-label { font-size:10px; text-anchor:middle; dominant-baseline:middle; fill:#fff; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h2>Node Balancer</h2>
|
|
<svg id="canvas"></svg>
|
|
<script>
|
|
const svg = document.getElementById("canvas");
|
|
const nodePositions = {};
|
|
const nodeY = 50;
|
|
const nodeWidth = 100;
|
|
const nodeHeight = 40;
|
|
let animationDuration = 2000;
|
|
|
|
// fetch animation duration
|
|
fetch("/api/config")
|
|
.then(res => res.json())
|
|
.then(cfg => { animationDuration = cfg.animationDurationMs; });
|
|
|
|
// fetch nodes
|
|
fetch("/api/nodes")
|
|
.then(res => res.json())
|
|
.then(nodeNames => {
|
|
nodeNames.forEach((name,i)=>{
|
|
const x = 50 + i*200;
|
|
nodePositions[name] = {x: x + nodeWidth/2, y: nodeY + nodeHeight/2};
|
|
|
|
// Node box
|
|
const rect = document.createElementNS("http://www.w3.org/2000/svg","rect");
|
|
rect.setAttribute("x", x);
|
|
rect.setAttribute("y", nodeY);
|
|
rect.setAttribute("width", nodeWidth);
|
|
rect.setAttribute("height", nodeHeight);
|
|
rect.setAttribute("class", "node");
|
|
svg.appendChild(rect);
|
|
|
|
// Node label
|
|
const label = document.createElementNS("http://www.w3.org/2000/svg","text");
|
|
label.setAttribute("x", x + nodeWidth/2);
|
|
label.setAttribute("y", nodeY + nodeHeight/2);
|
|
label.setAttribute("class","label");
|
|
label.textContent = name;
|
|
svg.appendChild(label);
|
|
});
|
|
});
|
|
|
|
fetch("/api/pods")
|
|
.then(res => res.json())
|
|
.then(podsByNode => {
|
|
for(const nodeName in podsByNode){
|
|
podsByNode[nodeName].forEach(podName => addPod(podName, nodeName));
|
|
}
|
|
});
|
|
|
|
// pod tracking per node
|
|
const podsPerNode = {};
|
|
|
|
// function to show pod vertically under node
|
|
function addPod(podName, nodeName) {
|
|
if(!nodePositions[nodeName]) return;
|
|
if(!podsPerNode[nodeName]) podsPerNode[nodeName] = [];
|
|
|
|
const podIndex = podsPerNode[nodeName].length;
|
|
const x = nodePositions[nodeName].x;
|
|
const y = nodeY + nodeHeight + 20 + podIndex*25;
|
|
|
|
const g = document.createElementNS("http://www.w3.org/2000/svg","g");
|
|
const circle = document.createElementNS("http://www.w3.org/2000/svg","circle");
|
|
circle.setAttribute("cx", x);
|
|
circle.setAttribute("cy", y);
|
|
circle.setAttribute("r", 10);
|
|
circle.setAttribute("class","pod");
|
|
g.appendChild(circle);
|
|
|
|
const text = document.createElementNS("http://www.w3.org/2000/svg","text");
|
|
text.setAttribute("x", x);
|
|
text.setAttribute("y", y);
|
|
text.setAttribute("class","pod-label");
|
|
text.textContent = podName;
|
|
g.appendChild(text);
|
|
|
|
svg.appendChild(g);
|
|
podsPerNode[nodeName].push(g);
|
|
}
|
|
|
|
// animate pod move
|
|
function movePod(podName, fromNode, toNode) {
|
|
const fromPods = podsPerNode[fromNode];
|
|
if(!fromPods) return;
|
|
|
|
// find pod element
|
|
let podG = null;
|
|
for(let i=0;i<fromPods.length;i++){
|
|
const g = fromPods[i];
|
|
const text = g.querySelector("text");
|
|
if(text && text.textContent === podName){
|
|
podG = g;
|
|
fromPods.splice(i,1); // remove from old node
|
|
break;
|
|
}
|
|
}
|
|
if(!podG) return;
|
|
|
|
// add to new node
|
|
if(!podsPerNode[toNode]) podsPerNode[toNode]=[];
|
|
podsPerNode[toNode].push(podG);
|
|
|
|
const steps = 60;
|
|
let progress=0;
|
|
const start = podG.querySelector("circle").getAttribute("cy")*1;
|
|
const end = nodeY + nodeHeight + 20 + (podsPerNode[toNode].length-1)*25;
|
|
const dx = nodePositions[toNode].x - nodePositions[fromNode].x;
|
|
const dy = end - start;
|
|
|
|
const anim = setInterval(()=>{
|
|
progress++;
|
|
const cx = nodePositions[fromNode].x + dx*progress/steps;
|
|
const cy = start + dy*progress/steps;
|
|
podG.querySelector("circle").setAttribute("cx", cx);
|
|
podG.querySelector("circle").setAttribute("cy", cy);
|
|
podG.querySelector("text").setAttribute("x", cx);
|
|
podG.querySelector("text").setAttribute("y", cy);
|
|
if(progress>=steps){
|
|
clearInterval(anim);
|
|
}
|
|
}, animationDuration/steps);
|
|
}
|
|
|
|
// SSE stream
|
|
const evtSource = new EventSource("/api/stream");
|
|
evtSource.onmessage = e => {
|
|
const evt = JSON.parse(e.data);
|
|
if(!evt.fromNode){
|
|
addPod(evt.pod, evt.toNode);
|
|
} else {
|
|
movePod(evt.pod, evt.fromNode, evt.toNode);
|
|
}
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|