diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/dashboard/dashboard.html | 100 |
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; |
