aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/dashboard/dashboard.html
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-05 02:05:02 +0100
committertv <tv@krebsco.de>2026-11-05 02:05:02 +0100
commitdf8dba88c5199e8f6822a6d134de5892ed011e60 (patch)
tree389874cb5f0cd98a8e512c3460df11296b755c20 /src/api/dashboard/dashboard.html
parent88aa2d13139d304e425177acaaa3b778a1e5d4fc (diff)
dashboard: don't update tooltips
Diffstat (limited to 'src/api/dashboard/dashboard.html')
-rw-r--r--src/api/dashboard/dashboard.html73
1 files changed, 66 insertions, 7 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index a2c862a..b2a5255 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -377,6 +377,58 @@ function el(tag, attrs, ...children) {
}
return node;
}
+// Sets `node`'s tooltip (its `title`; none if null), unless the pointer is on it: then
+// once the pointer leaves, so an open tooltip doesn't close and open again with every
+// update (as `tooltip` does on the map).
+const titlesDue = new Map();
+function retitle(node, title) {
+ if (node.getAttribute("title") === title) { titlesDue.delete(node); return; }
+ const set = value => value === null ? node.removeAttribute("title") : node.setAttribute("title", value);
+ if (!node.matches(":hover")) { titlesDue.delete(node); set(title); return; }
+ if (!titlesDue.has(node)) {
+ node.addEventListener("mouseleave", () => {
+ if (!titlesDue.has(node)) return;
+ const due = titlesDue.get(node);
+ titlesDue.delete(node);
+ set(due);
+ }, { once: true });
+ }
+ titlesDue.set(node, title);
+}
+// Makes `old` (an element) like `fresh`, one of the same tag, keeping its nodes where
+// they are alike, changing only what differs, and tooltips as `retitle` does: drawn
+// anew with every update, a table would close the tooltip the pointer is on, and open
+// it again. Listeners on `fresh` are not kept: put them on what stays.
+function morph(old, fresh) {
+ for (const attr of old.getAttributeNames()) {
+ if (!fresh.hasAttribute(attr)) {
+ if (attr === "title") retitle(old, null); else old.removeAttribute(attr);
+ }
+ }
+ for (const attr of fresh.getAttributeNames()) {
+ const value = fresh.getAttribute(attr);
+ if (attr === "title") retitle(old, value);
+ else if (old.getAttribute(attr) !== value) old.setAttribute(attr, value);
+ }
+ morphChildren(old, [...fresh.childNodes]);
+}
+// `parent`'s children made like `fresh` (`morph`).
+function morphChildren(parent, fresh) {
+ const old = [...parent.childNodes];
+ fresh.forEach((node, i) => {
+ const was = old[i];
+ if (was && was.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) {
+ if (was.nodeValue !== node.nodeValue) was.nodeValue = node.nodeValue;
+ } else if (was && was.nodeType === Node.ELEMENT_NODE && was.nodeName === node.nodeName) {
+ morph(was, node);
+ } else if (was) {
+ parent.replaceChild(node, was);
+ } else {
+ parent.append(node);
+ }
+ });
+ for (const extra of old.slice(fresh.length)) extra.remove();
+}
const SVG = "http://www.w3.org/2000/svg";
function svg(tag, attrs) {
const node = document.createElementNS(SVG, tag);
@@ -429,6 +481,15 @@ function focus(id) {
}
// --- Peers ---
+// On press (the table may be drawn anew, with ages, before a click would complete): the
+// row's peer in focus, or none if it was. On the table, as its rows are kept and
+// changed (`morph`), whoever they show.
+document.getElementById("peers").addEventListener("pointerdown", event => {
+ const row = event.target.closest("tr[data-peer]");
+ if (!row) return;
+ const id = row.getAttribute("data-peer");
+ focus(id === focused ? null : id);
+});
function renderPeers() {
const table = document.getElementById("peers");
const headers = { reaches: "How many members besides this node it reaches (an active " +
@@ -465,11 +526,9 @@ function renderPeers() {
reachesCell(peer.reaches, ours, id),
el("td", {}, peer.sameEpoch === null || peer.sameEpoch === undefined
? "" : peer.sameEpoch ? "yes" : "no"));
- // On press: the table may be drawn anew (ages) before a click would complete.
- row.addEventListener("pointerdown", () => focus(id === focused ? null : id));
rows.push(row);
}
- table.replaceChildren(...rows);
+ morphChildren(table, rows);
// As tall as the rows, up to 8 of them (and the header); then it scrolls.
const scroll = document.getElementById("peers-scroll");
scroll.style.maxHeight = rows.length > 9
@@ -572,7 +631,7 @@ function renderStats() {
const field = (key, value) => el("div", { class: "leaf" },
el("span", { class: "key" }, key + ": "), value);
title.replaceChildren("Message: ", what);
- title.title = title.textContent;
+ retitle(title, title.textContent);
document.getElementById("stats").replaceChildren(
field("at", clockTime(new Date(message.at))),
field(message.way === "received" ? "from" : "to", message.peer),
@@ -586,7 +645,7 @@ function renderStats() {
if (isPair(focused)) {
const [a, b] = focused.split(" ");
what.textContent = name(a) + " – " + name(b);
- what.title = focused;
+ retitle(what, focused);
const listed = (x, y) => (state.graph.intended[x] || []).includes(y);
shown = [el("div", { class: "leaf" }, el("span", { class: "key" }, "intended: "),
listed(a, b) || listed(b, a) ? "yes (member list)" : "no")];
@@ -611,7 +670,7 @@ function renderStats() {
destination: peer.destination, addresses: peer.addresses }, "paths"));
}
title.replaceChildren("Link: ", what);
- title.title = title.textContent;
+ retitle(title, title.textContent);
document.getElementById("stats").replaceChildren(...shown);
return;
}
@@ -631,7 +690,7 @@ function renderStats() {
if (report) shown.push(tree("links", report, "links"));
}
title.replaceChildren(focused === null ? "Figures: " : "Peer: ", what);
- title.title = title.textContent;
+ retitle(title, title.textContent);
document.getElementById("stats").replaceChildren(...shown);
}