mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
fix(dashboard): Network page in six columns with chat in its own, boxes that leave room, links spread along each side
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,13 +12,14 @@
|
||||
* (single connections, or instances) and `shown` (the ids of those matching the group's filter). Each shown member is
|
||||
* a link end of its own, and the open box itself has none; `graph.members` finds a member by id.
|
||||
*
|
||||
* layout(graph, width, moved, scroll) places the nodes in five columns in a fixed order, grows open boxes by their rows
|
||||
* layout(graph, width, moved, scroll) places the nodes in six columns in a fixed order, grows open boxes by their rows
|
||||
* and gives each link its path; rows in view of an open box's list are anchors (`anchors`).
|
||||
*/
|
||||
(function (root) {
|
||||
'use strict';
|
||||
|
||||
var COLUMNS = [0, 1, 2, 3, 4]; // game clients | auth, chat, worlds, others | master | dashboard, UGC | web clients
|
||||
var COLUMNS = [0, 1, 2, 3, 4, 5]; // game clients | auth, worlds, others | chat | master | dashboard, UGC | web clients
|
||||
// Chat has a column of its own between the worlds and master: every world has a link to it
|
||||
// An open group's box: its header, the filter, the rows (at most MAX_ROWS in view, the rest scroll) and a margin
|
||||
var GROUP = { head: 58, filter: 30, row: 30, maxRows: 8, foot: 6 }; // head: a name on up to two lines and one line under it
|
||||
|
||||
@@ -30,10 +31,10 @@
|
||||
function nodeOf(server) {
|
||||
switch (server.type) {
|
||||
case 'AUTH': return { id: 'auth', kind: 'auth', label: 'Auth', column: 1, order: 0 };
|
||||
case 'CHAT': return { id: 'chat', kind: 'chat', label: 'Chat', column: 1, order: 1 };
|
||||
case 'MASTER': return { id: 'master', kind: 'master', label: 'Master', column: 2, order: 0 };
|
||||
case 'DASHBOARD': return { id: 'dashboard', kind: 'dashboard', label: 'Dashboard', column: 3, order: 0 };
|
||||
case 'UGC': return { id: 'ugc', kind: 'ugc', label: 'UGC server', column: 3, order: 1 };
|
||||
case 'CHAT': return { id: 'chat', kind: 'chat', label: 'Chat', column: 2, order: 0 };
|
||||
case 'MASTER': return { id: 'master', kind: 'master', label: 'Master', column: 3, order: 0 };
|
||||
case 'DASHBOARD': return { id: 'dashboard', kind: 'dashboard', label: 'Dashboard', column: 4, order: 0 };
|
||||
case 'UGC': return { id: 'ugc', kind: 'ugc', label: 'UGC server', column: 4, order: 1 };
|
||||
case 'WORLD': {
|
||||
var zone = num(server.zone);
|
||||
// "World 1200 Nimbus Station #3" -> "World 1200 Nimbus Station"
|
||||
@@ -105,7 +106,7 @@
|
||||
function endOf(server) { return server.type === 'WORLD' && expanded[num(server.zone)] ? server.key : nodeOf(server).id; }
|
||||
|
||||
var clients = node({ id: 'clients', kind: 'clients', label: 'Game clients', column: 0, order: 0 });
|
||||
var web = node({ id: 'web', kind: 'web', label: 'Web clients', column: 4, order: 0 });
|
||||
var web = node({ id: 'web', kind: 'web', label: 'Web clients', column: 5, order: 0 });
|
||||
var hasChat = keys.some(function (k) { return servers[k].type === 'CHAT'; });
|
||||
|
||||
keys.forEach(function (key) {
|
||||
@@ -266,13 +267,14 @@
|
||||
* when closed), h its whole height; an open box's list starts at lists[id].top and anchors are the rows in view.
|
||||
*/
|
||||
function layout(graph, totalWidth, moved, scroll) {
|
||||
var nodeWidth = Math.max(140, Math.min(240, Math.floor(totalWidth / 5.4))), nodeHeight = 68, rowHeight = 88, pad = 16;
|
||||
// Boxes take at most 60% of the space between column centres, so there is always room for the links between them
|
||||
var nodeWidth = Math.max(120, Math.min(230, Math.floor((totalWidth - 32) / 9))), nodeHeight = 68, rowHeight = 88, pad = 16;
|
||||
var byColumn = COLUMNS.map(function () { return []; });
|
||||
graph.nodes.forEach(function (n) { byColumn[n.column].push(n); });
|
||||
var rows = Math.max.apply(null, byColumn.map(function (c) { return c.length; }).concat([1]));
|
||||
var height = rows * rowHeight + pad * 2;
|
||||
var usable = totalWidth - nodeWidth - pad * 2;
|
||||
var xs = [0, 0.28, 0.54, 0.78, 1].map(function (f) { return Math.round(pad + nodeWidth / 2 + f * usable); });
|
||||
var xs = [0, 0.2, 0.4, 0.6, 0.8, 1].map(function (f) { return Math.round(pad + nodeWidth / 2 + f * usable); });
|
||||
var heightOf = function (n) { return n.open ? groupHeight(n.shown.length) : nodeHeight; };
|
||||
var at = {};
|
||||
// Rows at a fixed pitch from the top, each column centred on the tallest; an open box pushes the boxes under it
|
||||
@@ -311,11 +313,35 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Links leaving a box's side spread down that side, ordered by where their other end is, instead of all meeting
|
||||
// at one point (an open box's rows keep their own row)
|
||||
var ends = {}; // "id|side" -> [{edge, end, otherY}]
|
||||
var endOf = function (id) { return at[id] || anchors[id]; };
|
||||
graph.edges.forEach(function (e) {
|
||||
var a = endOf(e.from), b = endOf(e.to);
|
||||
if (!a || !b || Math.abs(b.x - a.x) < nodeWidth * 0.75) return;
|
||||
var dir = b.x > a.x ? 1 : -1;
|
||||
if (!a.row) (ends[e.from + '|' + dir] = ends[e.from + '|' + dir] || []).push({ edge: e.id, end: 'a', other: b.y });
|
||||
if (!b.row) (ends[e.to + '|' + (-dir)] = ends[e.to + '|' + (-dir)] || []).push({ edge: e.id, end: 'b', other: a.y });
|
||||
});
|
||||
var ports = {}; // edgeId -> {a: y, b: y}
|
||||
Object.keys(ends).forEach(function (key) {
|
||||
var list = ends[key], box = at[key.split('|')[0]];
|
||||
if (!box) return;
|
||||
list.sort(function (p, q) { return p.other - q.other; });
|
||||
var top = box.y - nodeHeight / 2 + 12, span = Math.min(box.h, nodeHeight) - 24;
|
||||
list.forEach(function (p, i) {
|
||||
var y = list.length === 1 ? box.y : top + span * i / (list.length - 1);
|
||||
(ports[p.edge] = ports[p.edge] || {})[p.end] = Math.round(y);
|
||||
});
|
||||
});
|
||||
var withPort = function (p, y) { return y === undefined ? p : { x: p.x, y: y, h: p.h, column: p.column, row: p.row, port: true }; };
|
||||
var paths = {};
|
||||
graph.edges.forEach(function (e) {
|
||||
var a = at[e.from] || anchors[e.from], b = at[e.to] || anchors[e.to];
|
||||
var a = endOf(e.from), b = endOf(e.to);
|
||||
if (!a || !b) return;
|
||||
paths[e.id] = { fwd: curve(a, b, nodeWidth, 3.5, nodeHeight), back: curve(b, a, nodeWidth, 3.5, nodeHeight) };
|
||||
var pa = withPort(a, ports[e.id] && ports[e.id].a), pb = withPort(b, ports[e.id] && ports[e.id].b);
|
||||
paths[e.id] = { fwd: curve(pa, pb, nodeWidth, 3.5, nodeHeight), back: curve(pb, pa, nodeWidth, 3.5, nodeHeight) };
|
||||
});
|
||||
return { width: totalWidth, height: height, nodeWidth: nodeWidth, nodeHeight: nodeHeight, nodes: at, lists: lists, anchors: anchors, paths: paths };
|
||||
}
|
||||
|
||||
@@ -755,9 +755,10 @@ How it is counted:
|
||||
|
||||
### Network
|
||||
|
||||
**Network** (`health_view`, next to Diagnostics) draws the traffic live, from the same reports: game clients on the
|
||||
left, auth, chat, the worlds (one box per zone) and any other server that reports in the middle,
|
||||
then master, then the dashboard and the UGC server, and web clients (browsers and API users) right of the dashboard. Servers appear as they report, so a new kind of
|
||||
**Network** (`health_view`, next to Diagnostics) draws the traffic live, from the same reports, in six columns: game clients on the
|
||||
left, then auth, the worlds (one box per zone) and any other server that reports, then chat (every world links to it),
|
||||
then master, then the dashboard and the UGC server, and web clients (browsers and API users) on the right. Links leaving
|
||||
a side of a box spread down it in the order of their other ends, so they don't all meet at one point. Servers appear as they report, so a new kind of
|
||||
server shows up without changes. Each link has a lane each way, as thick as its bytes per second, with dashes moving
|
||||
faster with more packets, and coloured by its load against its own peak over the last 5 minutes; hover it for the
|
||||
numbers. Boxes show connections, average ping, resends, busy workers and live dashboard pages. Clicking a box shows its
|
||||
|
||||
@@ -39,7 +39,7 @@ const summary = {
|
||||
|
||||
const graph = G.build(summary, {});
|
||||
const ids = graph.nodes.map((n) => n.id);
|
||||
same(ids, ['clients', 'auth', 'chat', 'zone:1200', 'service:99:0:0', 'master', 'dashboard', 'ugc', 'web'], 'nodes in column order, one per zone');
|
||||
same(ids, ['clients', 'auth', 'zone:1200', 'service:99:0:0', 'chat', 'master', 'dashboard', 'ugc', 'web'], 'nodes in column order, one per zone, chat in its own column');
|
||||
const edge = (id) => graph.edges.find((e) => e.id === id);
|
||||
|
||||
same(edge('clients>zone:1200').fwd, { packets: 27, bytes: 2700 }, 'players to the zone: both instances');
|
||||
@@ -114,7 +114,7 @@ same(G.speed(1e6) < G.speed(1), true, 'busier moves faster');
|
||||
|
||||
// Layout: columns left to right, every link a path each way (the lane offset moves an end by up to 3.5 px)
|
||||
const L = G.layout(graph, 1000);
|
||||
same(L.nodes.clients.x < L.nodes.auth.x && L.nodes.auth.x < L.nodes.master.x && L.nodes.master.x < L.nodes.dashboard.x && L.nodes.dashboard.x < L.nodes.web.x, true, 'columns, web clients right of the dashboard');
|
||||
same(L.nodes.clients.x < L.nodes.auth.x && L.nodes.auth.x < L.nodes.chat.x && L.nodes.chat.x < L.nodes.master.x && L.nodes.master.x < L.nodes.dashboard.x && L.nodes.dashboard.x < L.nodes.web.x, true, 'columns, web clients right of the dashboard');
|
||||
|
||||
// A dragged box goes where it was put, its links follow, and the box grows the canvas if needed
|
||||
{
|
||||
@@ -169,3 +169,12 @@ if (failures) {
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('network-graph: all passed');
|
||||
|
||||
// Boxes never overlap the next column, and links leaving one side of a box spread down it
|
||||
{
|
||||
const W = G.layout(graph, 1650);
|
||||
const xs = [...new Set(Object.values(W.nodes).map(n => n.x))].sort((a, b) => a - b);
|
||||
for (let i = 1; i < xs.length; i++) same(xs[i] - xs[i - 1] >= W.nodeWidth + 40, true, 'room between columns ' + i);
|
||||
const starts = Object.keys(W.paths).filter(id => id.includes('master')).map(id => W.paths[id].fwd);
|
||||
same(new Set(starts.map(d => d.split(' ')[0])).size > 1 || starts.length < 2, true, 'links at master do not all meet at one point');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user