mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
feat(dashboard): drag the Network page's boxes around; links coloured against the busiest link
The layout the viewer makes is kept in the browser, with a Reset layout button. A link's colour now compares it with the busiest link right now instead of its own recent peak, which made steady links look saturated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -188,9 +188,10 @@
|
||||
|
||||
/**
|
||||
* Positions: columns across `width` (node centres), rows in each column spread over the tallest column's height.
|
||||
* `moved` ({id: {fx, y}}, optional) places nodes the viewer dragged: fx is a fraction of the width, y in pixels.
|
||||
* Returns {width, height, nodeWidth, nodeHeight, nodes: {id: {x, y}}, paths: {edgeId: {fwd, back}}}.
|
||||
*/
|
||||
function layout(graph, totalWidth) {
|
||||
function layout(graph, totalWidth, moved) {
|
||||
var nodeWidth = Math.max(110, Math.min(170, Math.floor(totalWidth / 6.4))), nodeHeight = 54, rowHeight = 74, pad = 16;
|
||||
var byColumn = COLUMNS.map(function () { return []; });
|
||||
graph.nodes.forEach(function (n) { byColumn[n.column].push(n); });
|
||||
@@ -203,6 +204,13 @@
|
||||
var step = (height - pad * 2) / Math.max(column.length, 1);
|
||||
column.forEach(function (n, i) { at[n.id] = { x: xs[c], y: Math.round(pad + step * (i + 0.5)), column: c }; });
|
||||
});
|
||||
Object.keys(moved || {}).forEach(function (id) {
|
||||
var m = moved[id], a = at[id];
|
||||
if (!a || !(m.fx >= 0) || !(m.y >= 0)) return;
|
||||
a.x = Math.round(Math.min(totalWidth - pad - nodeWidth / 2, Math.max(pad + nodeWidth / 2, m.fx * totalWidth)));
|
||||
a.y = Math.round(Math.max(pad + nodeHeight / 2, m.y));
|
||||
height = Math.max(height, a.y + nodeHeight / 2 + pad);
|
||||
});
|
||||
var paths = {};
|
||||
graph.edges.forEach(function (e) {
|
||||
var a = at[e.from], b = at[e.to];
|
||||
@@ -215,7 +223,7 @@
|
||||
// A curve from a's side to b's side, moved `offset` px to its left (so the two directions sit side by side)
|
||||
function curve(a, b, nodeWidth, offset) {
|
||||
var half = nodeWidth / 2, x1, x2, c1, c2;
|
||||
if (a.x === b.x) {
|
||||
if (Math.abs(a.x - b.x) < nodeWidth) {
|
||||
// Same column: bulge out on the side with room (left of the rightmost column, right of the others)
|
||||
var side = a.column === COLUMNS.length - 1 ? -1 : 1;
|
||||
x1 = x2 = a.x + side * half;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
{% block css %}
|
||||
<style>
|
||||
/* Load of a link against its own peak of the last 5 minutes */
|
||||
/* Load of a link against the busiest link right now */
|
||||
.nf { --nf-idle: var(--bs-secondary-color); --nf-low: var(--bs-info); --nf-mid: var(--bs-warning); --nf-high: var(--bs-danger); }
|
||||
.nf-box { position: relative; }
|
||||
.nf-svg { display: block; width: 100%; }
|
||||
@@ -17,7 +17,8 @@
|
||||
.nf-svg .load-high { stroke: var(--nf-high); }
|
||||
.nf-svg .edge .hit { fill: none; stroke: transparent; stroke-width: 16; cursor: help; }
|
||||
.nf-svg .node rect { fill: var(--bs-body-bg); stroke: var(--bs-border-color); stroke-width: 1.5; }
|
||||
.nf-svg .node { cursor: pointer; }
|
||||
.nf-svg .node { cursor: grab; touch-action: none; }
|
||||
.nf-svg.dragging, .nf-svg.dragging .node { cursor: grabbing; }
|
||||
.nf-svg .node:hover rect, .nf-svg .node:focus rect { stroke: var(--bs-primary); }
|
||||
.nf-svg .node.selected rect { stroke: var(--bs-primary); stroke-width: 2.5; }
|
||||
.nf-svg .node.gone rect { stroke-dasharray: 4 3; }
|
||||
@@ -48,7 +49,7 @@
|
||||
<div>
|
||||
<h2 class="mb-1">Network</h2>
|
||||
<p class="text-body-secondary mb-0 small">What goes between the players, the servers and web clients right now: each line is a link, one lane each way,
|
||||
as thick as its bytes per second and coloured by its load against its own peak of the last 5 minutes. Rates are over each server's last report (5 seconds).
|
||||
as thick as its bytes per second and coloured by its load against the busiest link right now. Drag the boxes to arrange them; the layout is kept in this browser. Rates are over each server's last report (5 seconds).
|
||||
Hover a line for its numbers, click a box for its details. History is on <a href="/diagnostics">Diagnostics</a>.</p>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
@@ -65,11 +66,11 @@
|
||||
<div class="col-xxl-9">
|
||||
<div class="card">
|
||||
<div class="card-header py-2 d-flex flex-wrap justify-content-between gap-2">
|
||||
<h5 class="mb-0 fs-6">Traffic flow</h5>
|
||||
<h5 class="mb-0 fs-6">Traffic flow <button type="button" class="btn btn-link btn-sm p-0 ms-2 align-baseline" id="nfResetLayout" title="Put every box back where it started">Reset layout</button></h5>
|
||||
<span class="nf-legend">
|
||||
<span class="me-2 text-nowrap"><span class="key" style="background:var(--nf-low)"></span>light</span>
|
||||
<span class="me-2 text-nowrap"><span class="key" style="background:var(--nf-mid)"></span>busy</span>
|
||||
<span class="me-2 text-nowrap"><span class="key" style="background:var(--nf-high)"></span>at its peak</span>
|
||||
<span class="me-2 text-nowrap"><span class="key" style="background:var(--nf-high)"></span>busiest</span>
|
||||
<span class="me-2 text-nowrap"><span class="key" style="background:repeating-linear-gradient(90deg,var(--nf-idle) 0 5px,transparent 5px 8px)"></span>estimated (worked out from totals)</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -178,13 +179,20 @@
|
||||
return lines.filter(Boolean);
|
||||
}
|
||||
|
||||
// Boxes the viewer dragged, kept in this browser: {id: {fx, y}}
|
||||
var moved = {}, lastLayout = null;
|
||||
try { moved = JSON.parse(localStorage.getItem('network.positions') || '{}') || {}; } catch (e) { moved = {}; }
|
||||
function saveMoved() { try { localStorage.setItem('network.positions', JSON.stringify(moved)); } catch (e) { /* private window */ } }
|
||||
var busiest = 0;
|
||||
function renderDiagram(width) {
|
||||
var L = G.layout(graph, width);
|
||||
var L = G.layout(graph, width, moved);
|
||||
lastLayout = L;
|
||||
busiest = graph.edges.reduce(function (m, e) { return Math.max(m, edgeValue(e)); }, 0);
|
||||
var html = '';
|
||||
graph.edges.forEach(function (e) {
|
||||
var p = L.paths[e.id];
|
||||
if (!p) return;
|
||||
var load = G.loadClass(edgeValue(e), peakOf(e.id));
|
||||
var load = G.loadClass(edgeValue(e), busiest);
|
||||
var lanes = [['fwd', e.fwd, e.bytesKnown.fwd], ['back', e.back, e.bytesKnown.back]];
|
||||
html += '<g class="edge' + (e.exact ? '' : ' estimated') + '" data-edge="' + esc(e.id) + '">';
|
||||
lanes.forEach(function (lane) {
|
||||
@@ -225,7 +233,7 @@
|
||||
var links = graph.edges.filter(function (e) { return e.from === n.id; }).sort(function (a, b) { return edgeValue(b) - edgeValue(a); });
|
||||
return '<div class="nf-row" data-node="' + esc(n.id) + '" role="button" tabindex="0"><div class="d-flex justify-content-between"><b>' + esc(n.label) + '</b><span class="small text-body-secondary">' + esc(nodeMeta(n).join(' · ')) + '</span></div>' +
|
||||
links.map(function (e) {
|
||||
var load = G.loadClass(edgeValue(e), peakOf(e.id));
|
||||
var load = G.loadClass(edgeValue(e), graph.edges.reduce(function (m, x) { return Math.max(m, edgeValue(x)); }, 0));
|
||||
return '<div class="small mt-1">→ ' + esc(labelOf(e.to)) + (e.exact ? '' : ' <span class="badge text-bg-secondary">estimated</span>') +
|
||||
'<div class="text-body-secondary">out ' + esc(rateText(e.fwd, e, e.bytesKnown.fwd)) + ' · back ' + esc(rateText(e.back, e, e.bytesKnown.back)) + '</div>' +
|
||||
'<div class="nf-bar" style="width:' + Math.max(2, edgeValue(e) / max * 100).toFixed(1) + '%;background:var(--nf-' + load + ')"></div></div>';
|
||||
@@ -275,7 +283,37 @@
|
||||
render();
|
||||
showDetail(n);
|
||||
}
|
||||
svg.addEventListener('click', onNode);
|
||||
// Dragging a box moves it (a short press is still a click)
|
||||
var drag = null, justDragged = false;
|
||||
function svgPoint(ev) {
|
||||
var r = svg.getBoundingClientRect(), vb = svg.viewBox.baseVal;
|
||||
return { x: (ev.clientX - r.left) * (vb.width / r.width), y: (ev.clientY - r.top) * (vb.height / r.height) };
|
||||
}
|
||||
svg.addEventListener('pointerdown', function (ev) {
|
||||
var el = ev.target.closest('[data-node]');
|
||||
if (!el || ev.button !== 0 || ev.target.closest('[data-toggle]') || !lastLayout) return;
|
||||
var id = el.getAttribute('data-node'), at = lastLayout.nodes[id], p = svgPoint(ev);
|
||||
if (!at) return;
|
||||
drag = { id: id, dx: at.x - p.x, dy: at.y - p.y, startX: ev.clientX, startY: ev.clientY, moving: false, pointer: ev.pointerId };
|
||||
});
|
||||
svg.addEventListener('pointermove', function (ev) {
|
||||
if (!drag || ev.pointerId !== drag.pointer) return;
|
||||
if (!drag.moving && Math.abs(ev.clientX - drag.startX) + Math.abs(ev.clientY - drag.startY) < 5) return;
|
||||
if (!drag.moving) { drag.moving = true; svg.classList.add('dragging'); try { svg.setPointerCapture(ev.pointerId); } catch (e) { /* ignore */ } }
|
||||
var p = svgPoint(ev);
|
||||
moved[drag.id] = { fx: (p.x + drag.dx) / lastLayout.width, y: p.y + drag.dy };
|
||||
renderDiagram(lastLayout.width);
|
||||
});
|
||||
function endDrag(ev) {
|
||||
if (!drag || (ev && ev.pointerId !== drag.pointer)) return;
|
||||
if (drag.moving) { justDragged = true; saveMoved(); setTimeout(function () { justDragged = false; }, 0); }
|
||||
svg.classList.remove('dragging');
|
||||
drag = null;
|
||||
}
|
||||
svg.addEventListener('pointerup', endDrag);
|
||||
svg.addEventListener('pointercancel', endDrag);
|
||||
document.getElementById('nfResetLayout').addEventListener('click', function () { moved = {}; saveMoved(); render(); });
|
||||
svg.addEventListener('click', function (ev) { if (justDragged) { ev.stopPropagation(); return; } onNode(ev); });
|
||||
stack.addEventListener('click', onNode);
|
||||
[svg, stack].forEach(function (el) {
|
||||
el.addEventListener('keydown', function (ev) { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); onNode(ev); } });
|
||||
|
||||
Reference in New Issue
Block a user