aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/dashboard/dashboard.html
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/dashboard/dashboard.html')
-rw-r--r--src/api/dashboard/dashboard.html100
1 files changed, 95 insertions, 5 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index e645b02..f286615 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -287,6 +287,31 @@ const PAGE_VERSION = "__PAGE_VERSION__";
let state = null;
let stateAt = 0;
+// --- Hand-off: a double click on a node opens its dashboard, on another host, as this
+// page was left (`handOff`): the map where it was, the view, the pins, the focus and the
+// settings this browser keeps for the page (another host's are its own). It comes in
+// the URL's fragment, which no request carries: taken here, before anything reads the
+// settings, and gone from the URL at once. ---
+const HAND_OFF = "#continuum=";
+const handOver = (() => {
+ if (!location.hash.startsWith(HAND_OFF)) return null;
+ try {
+ const handed = JSON.parse(decodeURIComponent(location.hash.slice(HAND_OFF.length)));
+ for (const [key, value] of Object.entries(handed.settings || {})) {
+ if (key.startsWith("continuum-") && typeof value === "string") {
+ try { localStorage.setItem(key, value); } catch (_) {}
+ }
+ }
+ return handed;
+ } catch (_) {
+ return null;
+ } finally {
+ try {
+ history.replaceState(null, "", location.pathname + location.search);
+ } catch (_) { /* not where history works */ }
+ }
+})();
+
// Fields that grow by themselves: the node's list (`GROWING` and `is_age` in its
// dashboard), filled in when it serves the page.
const GROWING = new Set(__GROWING__);
@@ -344,7 +369,7 @@ function age(secs) {
// What is in focus, its figures shown: a node's ID, or a link's pair key (`pairKey`,
// two IDs). null: this node's (`/admin/stats`).
-let focused = (() => {
+let focused = handOver ? handOver.focused || null : (() => {
try { return decodeURIComponent(location.hash.slice(1)) || null; } catch { return null; }
})();
const pairKey = (a, b) => a < b ? a + " " + b : b + " " + a;
@@ -406,6 +431,11 @@ function renderPeers() {
// --- Figures: a tree, its open branches kept across renders ---
const open = new Set(["stats", "peer", "links", "from", "to", "paths", "paths.destination"]);
+// As the page it was opened from had them (`handOver`).
+if (handOver && Array.isArray(handOver.figures)) {
+ open.clear();
+ for (const path of handOver.figures) if (typeof path === "string") open.add(path);
+}
function tree(key, value, path) {
// Empty: nothing to open.
if (value !== null && typeof value === "object" && Object.keys(value).length === 0) {
@@ -1026,6 +1056,11 @@ function renderGraph() {
// read again where the node says it changed (`document` events) ---
// What is open, by path (as JSON): the first level to begin with.
const documentOpen = new Set([JSON.stringify([])]);
+// As the page it was opened from had them (`handOver`): the same document, so the same
+// paths, opened as the tree is drawn.
+if (handOver && Array.isArray(handOver.document)) {
+ for (const path of handOver.document) if (typeof path === "string") documentOpen.add(path);
+}
// The open branches read, by path (as JSON): to read again on a change.
const documentBranches = new Map();
function storeUrl(path, shallow) {
@@ -1451,12 +1486,17 @@ document.addEventListener("selectionchange", release);
// The legend, hidden and shown again; the map takes the room.
const legendToggle = document.getElementById("legend-toggle");
-legendToggle.addEventListener("click", () => {
+function showLegend(shown) {
const legends = document.getElementById("legends");
- legends.hidden = !legends.hidden;
- legendToggle.textContent = legends.hidden ? "show legend" : "hide legend";
+ legends.hidden = !shown;
+ legendToggle.textContent = shown ? "hide legend" : "show legend";
+}
+legendToggle.addEventListener("click", () => {
+ showLegend(document.getElementById("legends").hidden);
if (state) renderGraph();
});
+// Hidden as on the page it was opened from (`handOver`).
+if (handOver && handOver.legend === false) showLegend(false);
// The members' links subscribed to (`live`: each tells of a change as it happens), or
// only asked for every 30 s; the node's choice, for every page (`POST
// /admin/dashboard/links`), shown as the state has it.
@@ -1500,6 +1540,55 @@ function moveView() {
"translate(" + view.x + " " + view.y + ") rotate(" + degrees(view.turn) + ") scale(" +
view.k + ")");
}
+// The map as the page it was opened from left it (`handOver`): its nodes where they
+// were (new ones are placed among them; on a dynamic map all then move to their places
+// smoothly, `settle`), the view and the pins.
+if (handOver) {
+ for (const [id, at] of Object.entries(handOver.positions || {})) {
+ const [x, y] = Array.isArray(at) ? at : [];
+ if (Number.isFinite(x) && Number.isFinite(y)) positions.set(id, { x, y });
+ }
+ for (const id of handOver.pinned || []) if (positions.has(id)) pinned.add(id);
+ if (positions.size > 0) relayout = false;
+ const handed = handOver.view || {};
+ for (const key of ["x", "y", "k", "turn"]) {
+ if (Number.isFinite(handed[key])) view[key] = handed[key];
+ }
+ moveView();
+}
+// The fragment that opens another node's dashboard (`id`'s) as this page is now
+// (`handOver`). What is in focus stays, but `id` itself: its page is its own.
+function handOff(id) {
+ const settings = {};
+ try {
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i);
+ if (key && key.startsWith("continuum-")) settings[key] = localStorage.getItem(key);
+ }
+ } catch (_) {}
+ const round = value => Math.round(value * 10) / 10;
+ return HAND_OFF + encodeURIComponent(JSON.stringify({
+ positions: Object.fromEntries([...positions].map(([node, p]) => [node, [round(p.x), round(p.y)]])),
+ pinned: [...pinned],
+ view: { ...view },
+ focused: focused === id ? null : focused,
+ settings,
+ legend: !document.getElementById("legends").hidden,
+ links: state.graph.live !== false,
+ figures: [...open],
+ document: [...documentOpen],
+ }));
+}
+// Links live or every 30 s as on the page it was opened from (`handOver`), once its
+// first state says otherwise. The node's choice: for every page open on it.
+let linksHandedOver = false;
+function handOverLinks() {
+ if (linksHandedOver || !handOver || typeof handOver.links !== "boolean") return;
+ linksHandedOver = true;
+ if ((state.graph.live !== false) !== handOver.links) {
+ fetch("dashboard/links?live=" + handOver.links, { method: "POST" }).catch(() => {});
+ }
+}
// `[x, y]` turned by `angle`.
function turned(x, y, angle) {
const c = Math.cos(angle), s = Math.sin(angle);
@@ -1664,7 +1753,7 @@ function openDashboard(id) {
const member = state.graph.members[id];
if (!member || member.self) return;
if (member.dashboard) {
- location.assign(member.dashboard);
+ location.assign(member.dashboard.split("#")[0] + handOff(id));
return;
}
const layer = document.getElementById("flashes");
@@ -2142,6 +2231,7 @@ function connect() {
if (state !== null) documentChanged({ all: true });
state = snapshot; stateAt = Date.now(); live = true;
status("live", true); pulse(); render(); trafficArrived();
+ handOverLinks();
});
events.addEventListener("patch", event => {
if (!state) return;