refactor(frontend): extract shared render/format helpers into util.js (#1451)
The tabs.js split's render-heavy domains (schedules, system, containers, questions/approvals) all share a handful of pure helpers that lived in the tabs.js IIFE: paintAtomic (atomic-swap render) and the fmtAgo / fmtElapsed / fmtDuration / truncate formatters (used 10/15/4/6/6 times across the file). Pull them into a new dashboard-internal util.js so the upcoming per-tab modules can import them instead of depending on the entry's closure — the helper analogue of the state.js roster keystone. They stay out of the cross-page common.js (their phrasing is dashboard-specific) but are now a shared dashboard module. All five are pure, so this is behaviour-preserving code motion; esbuild inlines util.js into the tabs.js bundle. Build green.
This commit is contained in:
parent
6734cab382
commit
3fda4ab127
3 changed files with 55 additions and 42 deletions
|
|
@ -46,10 +46,11 @@ export const form = (action, btnClass, btnLabel, confirmMsg, extra = {}, opts =
|
||||||
return f;
|
return f;
|
||||||
};
|
};
|
||||||
|
|
||||||
// `truncate`, `fmtAgo`, `fmtElapsed`, `fmtDuration` stay in tabs.js
|
// `truncate`, `fmtAgo`, `fmtElapsed`, `fmtDuration` + the `paintAtomic`
|
||||||
// for now — each has display-specific phrasing ("X running", "X ago")
|
// render helper live in the dashboard-internal `./util.js`, not here —
|
||||||
// tied to its caller, so they don't generalise cleanly. We can lift
|
// their phrasing ("X running", "X ago") is dashboard-specific, so they
|
||||||
// them when a second consumer needs the same shape.
|
// stay out of the cross-page `common.js` but are shared across the
|
||||||
|
// dashboard's own tab modules.
|
||||||
|
|
||||||
// ─── shared-worker SSE pipe ─────────────────────────────────────────────
|
// ─── shared-worker SSE pipe ─────────────────────────────────────────────
|
||||||
// Returns an EventSource-shaped facade backed by a SharedWorker that
|
// Returns an EventSource-shaped facade backed by a SharedWorker that
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
} from './common.js';
|
} from './common.js';
|
||||||
import { createTabStrip } from '@hive/shared/tabs.js';
|
import { createTabStrip } from '@hive/shared/tabs.js';
|
||||||
import { containersState, syncContainersFromSnapshot } from './state.js';
|
import { containersState, syncContainersFromSnapshot } from './state.js';
|
||||||
|
import { paintAtomic, fmtAgo, truncate, fmtElapsed, fmtDuration } from './util.js';
|
||||||
import {
|
import {
|
||||||
applyCapabilitiesChanged, applyToolGroupsChanged,
|
applyCapabilitiesChanged, applyToolGroupsChanged,
|
||||||
fetchAndRenderCapabilities, fetchAndRenderToolGroups,
|
fetchAndRenderCapabilities, fetchAndRenderToolGroups,
|
||||||
|
|
@ -43,16 +44,6 @@ window.marked = marked;
|
||||||
const CTX_WARN_TOKENS = 150_000; // fallback red threshold (≈ 75% of 200k)
|
const CTX_WARN_TOKENS = 150_000; // fallback red threshold (≈ 75% of 200k)
|
||||||
const CTX_CAUTION_TOKENS = 100_000; // fallback yellow threshold (≈ 50% of 200k)
|
const CTX_CAUTION_TOKENS = 100_000; // fallback yellow threshold (≈ 50% of 200k)
|
||||||
|
|
||||||
// Atomic-swap render helper: build into a DocumentFragment off-DOM,
|
|
||||||
// commit with one `replaceChildren`. See docs/web-ui.md::Atomic
|
|
||||||
// section repaint for why (no intermediate empty-state flash on
|
|
||||||
// poll cycles even when the builder allocates a lot of `el()`).
|
|
||||||
function paintAtomic(liveRoot, build) {
|
|
||||||
const buf = document.createDocumentFragment();
|
|
||||||
build(buf);
|
|
||||||
liveRoot.replaceChildren(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track which items we've already notified about so a re-render
|
// Track which items we've already notified about so a re-render
|
||||||
// doesn't re-fire for the same row. Keyed by stable ids; reset only
|
// doesn't re-fire for the same row. Keyed by stable ids; reset only
|
||||||
// when the page reloads.
|
// when the page reloads.
|
||||||
|
|
@ -2110,17 +2101,6 @@ window.marked = marked;
|
||||||
root.append(ul);
|
root.append(ul);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relative time, anchored to now. resolved_at is unix seconds (server-
|
|
||||||
// authored), so we don't have to worry about client/server clock skew
|
|
||||||
// for sub-minute precision.
|
|
||||||
function fmtAgo(unixSecs) {
|
|
||||||
const ageSec = Math.max(0, Math.floor(Date.now() / 1000 - unixSecs));
|
|
||||||
if (ageSec < 60) return ageSec + 's ago';
|
|
||||||
if (ageSec < 3600) return Math.floor(ageSec / 60) + 'm ago';
|
|
||||||
if (ageSec < 86400) return Math.floor(ageSec / 3600) + 'h ago';
|
|
||||||
return Math.floor(ageSec / 86400) + 'd ago';
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMetaInputs(s) {
|
function renderMetaInputs(s) {
|
||||||
const root = $('meta-inputs-section');
|
const root = $('meta-inputs-section');
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
|
|
@ -2230,10 +2210,6 @@ window.marked = marked;
|
||||||
root.append(form);
|
root.append(form);
|
||||||
}
|
}
|
||||||
|
|
||||||
function truncate(s, n) {
|
|
||||||
return s.length <= n ? s : s.slice(0, n - 1) + '…';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── rebuild queue ──────────────────────────────────────────────────────
|
// ─── rebuild queue ──────────────────────────────────────────────────────
|
||||||
// Keyed row cache for the rebuild-queue list. Maps entry.id → { el, fingerprint }.
|
// Keyed row cache for the rebuild-queue list. Maps entry.id → { el, fingerprint }.
|
||||||
// Same pattern as containerRowCache: reuse <li> nodes whose state hasn't
|
// Same pattern as containerRowCache: reuse <li> nodes whose state hasn't
|
||||||
|
|
@ -2461,12 +2437,6 @@ window.marked = marked;
|
||||||
return li;
|
return li;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtElapsed(secs) {
|
|
||||||
if (secs < 60) return secs + 's running';
|
|
||||||
if (secs < 3600) return Math.floor(secs / 60) + 'm ' + (secs % 60) + 's running';
|
|
||||||
return Math.floor(secs / 3600) + 'h ' + Math.floor((secs % 3600) / 60) + 'm running';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tick once per second to refresh "running Xs" badges in place
|
// Tick once per second to refresh "running Xs" badges in place
|
||||||
// (mirrors the question-TTL ticker pattern above).
|
// (mirrors the question-TTL ticker pattern above).
|
||||||
// Tick rebuild-queue elapsed-time badges once per second.
|
// Tick rebuild-queue elapsed-time badges once per second.
|
||||||
|
|
@ -2605,13 +2575,6 @@ window.marked = marked;
|
||||||
root.append(ul);
|
root.append(ul);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function fmtDuration(secs) {
|
|
||||||
if (secs < 60) return secs + 's';
|
|
||||||
if (secs < 3600) return Math.floor(secs / 60) + 'm ' + (secs % 60) + 's';
|
|
||||||
if (secs < 86400) return Math.floor(secs / 3600) + 'h ' + Math.floor((secs % 3600) / 60) + 'm';
|
|
||||||
return Math.floor(secs / 86400) + 'd ' + Math.floor((secs % 86400) / 3600) + 'h';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── scheduled prompts ─────────────────────────────────────────────────
|
// ─── scheduled prompts ─────────────────────────────────────────────────
|
||||||
// Backend exposes `/api/schedules` (snapshot), `/api/schedules`
|
// Backend exposes `/api/schedules` (snapshot), `/api/schedules`
|
||||||
// (POST, operator-direct submit), `/api/schedules/{id}/cancel`
|
// (POST, operator-direct submit), `/api/schedules/{id}/cancel`
|
||||||
|
|
|
||||||
49
frontend/packages/dashboard/src/util.js
Normal file
49
frontend/packages/dashboard/src/util.js
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Shared dashboard render + format helpers.
|
||||||
|
//
|
||||||
|
// Small pure utilities used across most dashboard tabs (the container
|
||||||
|
// tree, the rebuild queue, questions / approvals, schedules + reminders):
|
||||||
|
// an atomic-swap render helper and a handful of relative-time / duration
|
||||||
|
// formatters. They live here — a dashboard-internal module — rather than
|
||||||
|
// in the cross-page `common.js`, because they're specific to the
|
||||||
|
// dashboard's render style and not needed by the stand-alone pages.
|
||||||
|
//
|
||||||
|
// All pure: no DOM/module state captured, so any tab module can import
|
||||||
|
// them freely without ordering concerns.
|
||||||
|
|
||||||
|
// Atomic-swap render: build into an off-DOM DocumentFragment, then commit
|
||||||
|
// with a single `replaceChildren`. Avoids an intermediate empty-state
|
||||||
|
// flash on poll cycles even when the builder allocates a lot of nodes.
|
||||||
|
export function paintAtomic(liveRoot, build) {
|
||||||
|
const buf = document.createDocumentFragment();
|
||||||
|
build(buf);
|
||||||
|
liveRoot.replaceChildren(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relative age of a unix timestamp, coarsened to one unit ("5m ago").
|
||||||
|
export function fmtAgo(unixSecs) {
|
||||||
|
const ageSec = Math.max(0, Math.floor(Date.now() / 1000 - unixSecs));
|
||||||
|
if (ageSec < 60) return ageSec + 's ago';
|
||||||
|
if (ageSec < 3600) return Math.floor(ageSec / 60) + 'm ago';
|
||||||
|
if (ageSec < 86400) return Math.floor(ageSec / 3600) + 'h ago';
|
||||||
|
return Math.floor(ageSec / 86400) + 'd ago';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate a string to `n` chars, appending an ellipsis when clipped.
|
||||||
|
export function truncate(s, n) {
|
||||||
|
return s.length <= n ? s : s.slice(0, n - 1) + '…';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Running-duration label for in-flight items ("3m 12s running").
|
||||||
|
export function fmtElapsed(secs) {
|
||||||
|
if (secs < 60) return secs + 's running';
|
||||||
|
if (secs < 3600) return Math.floor(secs / 60) + 'm ' + (secs % 60) + 's running';
|
||||||
|
return Math.floor(secs / 3600) + 'h ' + Math.floor((secs % 3600) / 60) + 'm running';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compact duration label, two units deep ("1h 5m", "2d 3h").
|
||||||
|
export function fmtDuration(secs) {
|
||||||
|
if (secs < 60) return secs + 's';
|
||||||
|
if (secs < 3600) return Math.floor(secs / 60) + 'm ' + (secs % 60) + 's';
|
||||||
|
if (secs < 86400) return Math.floor(secs / 3600) + 'h ' + Math.floor((secs % 3600) / 60) + 'm';
|
||||||
|
return Math.floor(secs / 86400) + 'd ' + Math.floor((secs % 86400) / 3600) + 'h';
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue