aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/dashboard/dashboard.html
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-02 22:05:50 +0100
committertv <tv@krebsco.de>2026-11-02 22:05:50 +0100
commitdf314df4793765ea5334ac75b97d415286cc7fa3 (patch)
tree1ed6c10c066fc5b273c7cf04a1b341e70e38c4a8 /src/api/dashboard/dashboard.html
parent627efc3b2620d75fcf2deab170187ba2e082417c (diff)
dashboard: double-click opens peer dashboard
Diffstat (limited to 'src/api/dashboard/dashboard.html')
-rw-r--r--src/api/dashboard/dashboard.html67
1 files changed, 61 insertions, 6 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index 7125ad9..e645b02 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -252,10 +252,11 @@
class="toggle">messages: on</button></div>
<div class="legend">
<span class="hint">drag to pan, wheel or pinch to zoom, turn with two fingers;
- drag a node to move it, middle-click it to pin it there or let it go</span>
+ drag a node to move it, middle-click it to pin it there or let it go,
+ double-click it for its dashboard</span>
</div>
</div>
- <svg id="graph"><g id="view"><g id="flows" pointer-events="none"></g></g></svg>
+ <svg id="graph"><g id="view"><g id="flows" pointer-events="none"></g><g id="flashes" pointer-events="none"></g></g></svg>
</section>
<section id="peers-box">
<h2>Peers</h2>
@@ -1613,10 +1614,13 @@ graph.addEventListener("pointerdown", event => {
// Not the browser's drag and drop of the picture.
graph.addEventListener("dragstart", event => event.preventDefault());
// A node: dragged, it moves, held where the pointer is (`grabbed`) until let go; a click,
-// it is in focus. A middle click pins it where it is (`pinned`: the layout leaves it
+// it is in focus; a second one soon after opens its dashboard (`openDashboard`). A
+// middle click pins it where it is (`pinned`: the layout leaves it
// there), or lets it go if pinned (`unpin`); dragged with the middle button, it is
// pinned where it's dropped. Told apart here: the map holds the pointer while pressed
// (`pressing`), so the browser's clicks go to the map, not the node.
+let lastNodeClick = { id: null, at: 0 };
+const DOUBLE_CLICK_MS = 400;
let grabbed = null;
// Of the press under way on a node: whether it's the middle button's, and whether it
// moved the node.
@@ -1638,11 +1642,62 @@ function drag(event, id) {
// Dynamic: the others follow it at once.
if (byRtt) settle();
}, () => {
- if (!middlePress) focus(id);
- else if (pinned.has(id)) unpin(id);
- else pin(id);
+ if (middlePress) {
+ if (pinned.has(id)) unpin(id); else pin(id);
+ return;
+ }
+ const now = performance.now();
+ if (lastNodeClick.id === id && now - lastNodeClick.at < DOUBLE_CLICK_MS) {
+ lastNodeClick = { id: null, at: 0 };
+ openDashboard(id);
+ } else {
+ lastNodeClick = { id, at: now };
+ focus(id);
+ }
});
}
+// A node's dashboard, where it publishes one (`--dashboard-url`, in the member list),
+// opened in this page; else its halo flashes red, larger than usual, and "no dashboard"
+// rises from it in the same red, both fading out. This node's own: here already.
+const FLASH_MS = 1200;
+function openDashboard(id) {
+ const member = state.graph.members[id];
+ if (!member || member.self) return;
+ if (member.dashboard) {
+ location.assign(member.dashboard);
+ return;
+ }
+ const layer = document.getElementById("flashes");
+ const halo = svg("circle", { r: 17, fill: "var(--bad)", "fill-opacity": .5 });
+ const words = svg("text", { "text-anchor": "middle", "font-size": 12, "font-weight": 600,
+ fill: "var(--bad)", "fill-opacity": .5 });
+ words.textContent = "no dashboard";
+ layer.append(halo, words);
+ const started = performance.now();
+ const step = now => {
+ const p = positions.get(id);
+ const t = Math.min(1, Math.max(0, (now - started) / FLASH_MS));
+ if (!p || t >= 1) {
+ halo.remove();
+ words.remove();
+ return;
+ }
+ const opacity = String(1 - t);
+ halo.setAttribute("cx", p.x);
+ halo.setAttribute("cy", p.y);
+ halo.setAttribute("opacity", opacity);
+ // Up on the screen, however the map is turned or zoomed: from above the node, slowly.
+ const [x, y] = turned(0, -(22 + (calm.matches ? 0 : 30 * t)), -view.turn);
+ const attrs = upright({ x: p.x + x / view.k, y: p.y + y / view.k });
+ words.setAttribute("x", attrs.x);
+ words.setAttribute("y", attrs.y);
+ if (attrs.transform) words.setAttribute("transform", attrs.transform);
+ else words.removeAttribute("transform");
+ words.setAttribute("opacity", opacity);
+ requestAnimationFrame(step);
+ };
+ requestAnimationFrame(step);
+}
// A dragged node let go of: dynamic, it moves to its place among the others again;
// dragged with the middle button, it is pinned there.
function dropped() {