aboutsummaryrefslogtreecommitdiffstats
path: root/src/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/api')
-rw-r--r--src/api/dashboard/dashboard.html129
1 files changed, 82 insertions, 47 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index dd764b6..9f191be 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -429,8 +429,8 @@ function layout(nodes, pairs, width, height) {
}
}
-// The node or link (its pair key) under the pointer, if any. The map is drawn anew every second, so it is
-// kept here rather than left to :hover.
+// The node or link (its pair key) under the pointer, if any. Kept here rather than
+// left to :hover: its halo is an element of its own, drawn behind it.
let hovered = null;
function hover(id) {
if (hovered === id) return;
@@ -442,6 +442,45 @@ function hoverable(element, id) {
element.addEventListener("mouseleave", () => hover(null));
}
+// The map's elements by what they show (a key), kept across renders and updated in
+// place: the one under the pointer stays itself.
+const drawn = new Map();
+// The element `key` names, with exactly `attrs`; `made` sets up a new one (its
+// listeners).
+function drawing(key, tag, attrs, made) {
+ let node = drawn.get(key);
+ if (!node) {
+ node = svg(tag);
+ if (made) made(node);
+ drawn.set(key, node);
+ }
+ for (const attr of node.getAttributeNames()) if (!(attr in attrs)) node.removeAttribute(attr);
+ for (const [attr, value] of Object.entries(attrs)) {
+ if (node.getAttribute(attr) !== String(value)) node.setAttribute(attr, value);
+ }
+ return node;
+}
+function text(node, content) {
+ if (node.textContent !== content) node.textContent = content;
+}
+function tooltip(node, content) {
+ let title = node.firstElementChild;
+ if (!title) node.append(title = svg("title"));
+ text(title, content);
+}
+// `root` showing `children`, in order: the others go, and only what is out of place
+// moves.
+function show(root, children) {
+ const shown = new Set(children);
+ for (const [key, node] of drawn) {
+ if (!shown.has(node)) { node.remove(); drawn.delete(key); }
+ }
+ let at = root.firstChild;
+ for (const node of children) {
+ if (node === at) at = at.nextSibling; else root.insertBefore(node, at);
+ }
+}
+
let lastShape = "";
function renderGraph() {
const root = document.getElementById("graph");
@@ -462,19 +501,19 @@ function renderGraph() {
// Behind a link: a halo while it's hovered or in focus (`width`: of its lines).
const halo = (key, p, q, width) => {
if (key !== hovered && key !== focused) return;
- children.push(svg("line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y, stroke: "var(--self)",
- "stroke-opacity": key === focused ? .35 : .15, "stroke-width": width + 8,
- "stroke-linecap": "round" }));
+ children.push(drawing("halo " + key, "line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y,
+ stroke: "var(--self)", "stroke-opacity": key === focused ? .35 : .15,
+ "stroke-width": width + 8, "stroke-linecap": "round" }));
};
// Over a link: an invisible, wider line to point at and click.
const hits = [];
const hit = (key, p, q, width) => {
- const line = svg("line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y, stroke: "transparent",
- "stroke-width": width + 8 });
- line.style.cursor = "pointer";
- // On press: the map may be drawn anew before a click would complete.
- line.addEventListener("pointerdown", event => { event.stopPropagation(); focus(key); });
- hoverable(line, key);
+ const line = drawing("hit " + key, "line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y,
+ stroke: "transparent", "stroke-width": width + 8, cursor: "pointer" }, line => {
+ // On press: the map may be drawn anew before a click would complete.
+ line.addEventListener("pointerdown", event => { event.stopPropagation(); focus(key); });
+ hoverable(line, key);
+ });
hits.push(line);
return line;
};
@@ -482,11 +521,9 @@ function renderGraph() {
if (measured.has(key)) continue;
const p = positions.get(a), q = positions.get(b);
halo(key, p, q, 1);
- children.push(svg("line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y,
+ children.push(drawing("intended " + key, "line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y,
stroke: "var(--intended)", "stroke-dasharray": "4 4", "stroke-width": 1 }));
- const tooltip = svg("title");
- tooltip.textContent = name(a) + " – " + name(b) + ": intended (member list), not measured";
- hit(key, p, q, 1).append(tooltip);
+ tooltip(hit(key, p, q, 1), name(a) + " – " + name(b) + ": intended (member list), not measured");
}
const ms = rtt => rtt < 10 ? rtt.toFixed(1) : String(Math.round(rtt));
for (const [key, { a, b, link, secsAgo }] of measured) {
@@ -507,49 +544,49 @@ function renderGraph() {
const silent = !(a in state.graph.measured) || !(b in state.graph.measured);
// Dormant: a red underlay as wide as the lines, as a dormant node's red outline.
if (link.dormant) {
- children.push(svg("line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y, stroke: "var(--bad)",
- "stroke-opacity": .5, "stroke-width": width + 4, "stroke-linecap": "round" }));
+ children.push(drawing("underlay " + key, "line", { x1: p.x, y1: p.y, x2: q.x, y2: q.y,
+ stroke: "var(--bad)", "stroke-opacity": .5, "stroke-width": width + 4,
+ "stroke-linecap": "round" }));
}
lines.forEach((line, i) => {
// 0, then +1, -1, +2, -2 … steps of 6 px to the side.
const side = i === 0 ? 0 : (i % 2 ? 1 : -1) * Math.ceil(i / 2) * 6;
const ox = nx * side, oy = ny * side;
- const drawn = svg("line", { x1: p.x + ox, y1: p.y + oy, x2: q.x + ox, y2: q.y + oy,
+ const attrs = { x1: p.x + ox, y1: p.y + oy, x2: q.x + ox, y2: q.y + oy,
stroke: colourOf(line.transport),
// Narrow where dormant (over its underlay).
- "stroke-width": link.dormant ? (line.used ? 1.5 : 0.8) : line.used ? 3 : 1.2 });
+ "stroke-width": link.dormant ? (line.used ? 1.5 : 0.8) : line.used ? 3 : 1.2 };
// Dotted where an end reports nothing, dashed where relayed, solid where direct.
if (silent) {
- drawn.setAttribute("stroke-dasharray", line.used ? "1 5" : "1 3");
- drawn.setAttribute("stroke-linecap", "round");
+ attrs["stroke-dasharray"] = line.used ? "1 5" : "1 3";
+ attrs["stroke-linecap"] = "round";
} else if (line.used ? link.relayed : line.relayed) {
- drawn.setAttribute("stroke-dasharray", "7 4");
+ attrs["stroke-dasharray"] = "7 4";
}
- const title = svg("title");
- title.textContent = name(a) + " – " + name(b) +
+ const drawnLine = drawing("line " + key + " " + i, "line", attrs);
+ const description = name(a) + " – " + name(b) +
(line.transport ? " via " + line.transport : "") + (line.used ? " (in use)" : "") +
((line.used ? link.relayed : line.relayed) ? ", relayed" : "") + (link.dormant ? ", dormant" : "") +
(silent ? ", one end reports no links" : "") +
(typeof line.rttMs === "number" ? ", " + line.rttMs.toFixed(1) + " ms" : "") +
", measured " + age(secsAgo + elapsed()) + " ago";
- descriptions.push(title.textContent);
- children.push(drawn);
+ tooltip(drawnLine, description);
+ descriptions.push(description);
+ children.push(drawnLine);
if (typeof line.rttMs === "number") {
// The one in use at the middle, larger; the others further along, beside theirs.
const t = line.used ? .5 : .3;
const out = line.used ? -4 : Math.sign(side) * 9;
- const label = svg("text", {
+ const label = drawing("rtt " + key + " " + i, "text", {
x: p.x + (q.x - p.x) * t + ox + nx * out,
y: p.y + (q.y - p.y) * t + oy + ny * out + (line.used ? 0 : 3),
"text-anchor": "middle", "font-size": line.used ? 11 : 9,
fill: line.used ? "var(--fg)" : "var(--muted)" });
- label.textContent = ms(line.rttMs);
+ text(label, ms(line.rttMs));
children.push(label);
}
});
- const tooltip = svg("title");
- tooltip.textContent = descriptions.join("\n");
- covering.append(tooltip);
+ tooltip(covering, descriptions.join("\n"));
}
// Above the lines and their labels, below the nodes.
children.push(...hits);
@@ -560,29 +597,27 @@ function renderGraph() {
// A link in focus: both ends ringed.
// A halo as links have: light under the pointer, stronger in focus.
if (shown || id === hovered) {
- children.push(svg("circle", { cx: p.x, cy: p.y, r: 13, fill: "var(--self)",
- "fill-opacity": shown ? .35 : .15, stroke: "none" }));
+ children.push(drawing("ring " + id, "circle", { cx: p.x, cy: p.y, r: 13,
+ fill: "var(--self)", "fill-opacity": shown ? .35 : .15, stroke: "none" }));
}
+ const pressed = element => {
+ element.addEventListener("pointerdown", event => { event.stopPropagation(); focus(id); });
+ hoverable(element, id);
+ };
// Dormant as this node sees it (`/peers`): a red outline.
const dormant = (state.peers.peers[id] || {}).status === "dormant";
- const circle = svg("circle", { cx: p.x, cy: p.y, r: member.self ? 8 : 6,
+ const circle = drawing("node " + id, "circle", { cx: p.x, cy: p.y, r: member.self ? 8 : 6,
fill: member.self ? "var(--self)" : reported ? "var(--fg)" : "var(--panel)",
stroke: member.self ? "var(--self)" : dormant ? "var(--bad)" : "var(--fg)",
- "stroke-width": dormant ? 2.5 : 1.5 });
- const title = svg("title");
- title.textContent = id + (reported ? "" : " (no links reported)") +
- (dormant ? " (dormant)" : "");
- circle.append(title);
- circle.addEventListener("pointerdown", event => { event.stopPropagation(); focus(id); });
- hoverable(circle, id);
- const label = svg("text", { x: p.x + 10, y: p.y + 4, "font-size": 12, fill: "var(--fg)" });
- label.textContent = name(id);
- label.style.cursor = "pointer";
- label.addEventListener("pointerdown", event => { event.stopPropagation(); focus(id); });
- hoverable(label, id);
+ "stroke-width": dormant ? 2.5 : 1.5 }, pressed);
+ tooltip(circle, id + (reported ? "" : " (no links reported)") +
+ (dormant ? " (dormant)" : ""));
+ const label = drawing("name " + id, "text", { x: p.x + 10, y: p.y + 4, "font-size": 12,
+ fill: "var(--fg)", cursor: "pointer" }, pressed);
+ text(label, name(id));
children.push(circle, label);
}
- root.replaceChildren(...children);
+ show(root, children);
}
// --- Document: read a level at a time (`/store?shallow=true`) as it is opened, and