mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-23 21:22:49 +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
|
* (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.
|
* 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`).
|
* and gives each link its path; rows in view of an open box's list are anchors (`anchors`).
|
||||||
*/
|
*/
|
||||||
(function (root) {
|
(function (root) {
|
||||||
'use strict';
|
'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
|
// 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
|
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) {
|
function nodeOf(server) {
|
||||||
switch (server.type) {
|
switch (server.type) {
|
||||||
case 'AUTH': return { id: 'auth', kind: 'auth', label: 'Auth', column: 1, order: 0 };
|
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 'CHAT': return { id: 'chat', kind: 'chat', label: 'Chat', column: 2, order: 0 };
|
||||||
case 'MASTER': return { id: 'master', kind: 'master', label: 'Master', 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: 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: 3, order: 1 };
|
case 'UGC': return { id: 'ugc', kind: 'ugc', label: 'UGC server', column: 4, order: 1 };
|
||||||
case 'WORLD': {
|
case 'WORLD': {
|
||||||
var zone = num(server.zone);
|
var zone = num(server.zone);
|
||||||
// "World 1200 Nimbus Station #3" -> "World 1200 Nimbus Station"
|
// "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; }
|
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 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'; });
|
var hasChat = keys.some(function (k) { return servers[k].type === 'CHAT'; });
|
||||||
|
|
||||||
keys.forEach(function (key) {
|
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.
|
* 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) {
|
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 []; });
|
var byColumn = COLUMNS.map(function () { return []; });
|
||||||
graph.nodes.forEach(function (n) { byColumn[n.column].push(n); });
|
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 rows = Math.max.apply(null, byColumn.map(function (c) { return c.length; }).concat([1]));
|
||||||
var height = rows * rowHeight + pad * 2;
|
var height = rows * rowHeight + pad * 2;
|
||||||
var usable = totalWidth - nodeWidth - 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 heightOf = function (n) { return n.open ? groupHeight(n.shown.length) : nodeHeight; };
|
||||||
var at = {};
|
var at = {};
|
||||||
// Rows at a fixed pitch from the top, each column centred on the tallest; an open box pushes the boxes under it
|
// 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 = {};
|
var paths = {};
|
||||||
graph.edges.forEach(function (e) {
|
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;
|
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 };
|
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
|
||||||
|
|
||||||
**Network** (`health_view`, next to Diagnostics) draws the traffic live, from the same reports: game clients on the
|
**Network** (`health_view`, next to Diagnostics) draws the traffic live, from the same reports, in six columns: game clients on the
|
||||||
left, auth, chat, the worlds (one box per zone) and any other server that reports in the middle,
|
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) right of the dashboard. Servers appear as they report, so a new kind of
|
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
|
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
|
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
|
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 graph = G.build(summary, {});
|
||||||
const ids = graph.nodes.map((n) => n.id);
|
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);
|
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');
|
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)
|
// 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);
|
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
|
// 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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
console.log('network-graph: all passed');
|
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