dr-sandbox/app/web/static/run-modal.js
Michael Pilosov 44de8deeeb viz: extract N-panel-agnostic module; homepage modal reuses it for single-run view
- panel-grid.js (new): exports mountPanels({host, controls, stems}) → {destroy}.
  Moved createPanel + shared control wiring + linked-hover + pad-to-match
  time mapping out of compare.js. Stem-count-agnostic; works for 1, 2, or N.
- Panel DOM is cloned from <template id=compare-panel-tpl> on each page.
- compare.js is now a ~10-line shim: parse ?a=&b=, call mountPanels.
- Per-panel color is viridis-sampled by index/N (middle viridis for N=1,
  ends-of-palette for N=2, linear lerp for N≤8, cycle at N≥9). Set as
  --panel-color on the panel element; CSS reads it for tag/time-seg.
- Homepage <dialog id=run-modal> + run-modal.js hijack the 'embedding' link
  (plain click → modal; meta/ctrl/middle-click still opens plotly HTML).
  Dialog close disposes every panel's renderer/geometry/material.
- .compare-grid → repeat(auto-fit, minmax(360px, 1fr)) handles N=1..many,
  replaces the <900px one-column media rule.
- Runs list: relabel Prefect's 'Late' state as 'Queued' — more honest
  description of what the runner is doing at the concurrency cap.
2026-04-22 16:17:01 -06:00

70 lines
2.5 KiB
JavaScript

// run-modal.js — homepage click-hijack for embedding links. Opens a
// <dialog id="run-modal"> that renders the run's embedding via panel-grid.js.
import { mountPanels } from './panel-grid.js?v=1';
const dialog = document.getElementById('run-modal');
const host = document.getElementById('modal-panel-host');
const controls = document.getElementById('modal-compare-controls');
const closeBtn = document.getElementById('run-modal-close');
const runsSlot = document.getElementById('runs-slot');
let active = null; // { destroy } returned by mountPanels
function openFor(stem) {
if (!dialog || !host || !controls) return;
if (active) { try { active.destroy(); } catch (_) {} active = null; }
active = mountPanels({ host, controls, stems: [stem] });
if (typeof dialog.showModal === 'function') dialog.showModal();
else dialog.setAttribute('open', '');
}
function closeModal() {
if (!dialog) return;
if (dialog.open) dialog.close();
if (active) { try { active.destroy(); } catch (_) {} active = null; }
}
function onRunsClick(ev) {
if (ev.defaultPrevented) return;
if (ev.button !== undefined && ev.button !== 0) return;
if (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
const a = ev.target.closest('a[data-role="embedding-link"]');
if (!a) return;
if (!runsSlot || !runsSlot.contains(a)) return;
const stem = a.dataset.stem;
if (!stem) return;
ev.preventDefault();
openFor(stem);
}
function wire() {
if (!dialog) return;
if (runsSlot) runsSlot.addEventListener('click', onRunsClick);
if (closeBtn) closeBtn.addEventListener('click', closeModal);
dialog.addEventListener('close', () => {
if (active) { try { active.destroy(); } catch (_) {} active = null; }
});
// Clicking the backdrop (native behavior fires a click on the dialog
// itself, with target === dialog) closes the modal.
dialog.addEventListener('click', (ev) => {
if (ev.target === dialog) closeModal();
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', wire);
} else {
wire();
}
// htmx re-renders #runs-slot every 3s. Delegation on #runs-slot survives the
// swap (the slot element itself is stable), so we don't need to re-bind —
// but we keep the hook in case a consumer ever replaces the slot wholesale.
document.body.addEventListener('htmx:afterSwap', (ev) => {
if (ev.detail?.target?.id === 'runs-slot' && runsSlot && !runsSlot._runModalWired) {
runsSlot.addEventListener('click', onRunsClick);
runsSlot._runModalWired = true;
}
});