Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ auto-generated per-PR notes; this file is the curated, human-readable history.
## [Unreleased]

### Changed
- **Dashboard tile/filter runtime extracted into a route-scoped
`DashboardSession`** (#276 Phase 3b). Wave generations, per-slot
cancellation, the 6-way tile pool, filter waves/merging, and the retry
cascade now live in `src/ui/dashboard/dashboard-session.ts` — constructible
without the `App` object, fed an explicit `DashboardRuntimeInput` (built by
the shell from the favorites list, so a stored dashboard document can
replace that source later), with every DOM write behind injected shell
hooks. `renderDashboard(app)` keeps its signature as the integration entry
(the existing DOM-driven dashboard suite passed unmodified). `destroy()`
aborts all in-flight tile/KPI/filter work, tears down chart instances,
disposes the filter bar, and turns later entry points into no-ops — closing
the orphaned-debounce-timer gap (`buildFilterBar` now returns
`{ el, dispose }`; both the dashboard and the detached Data view dispose the
previous bar on re-render). A stale-generation guard on streamed progress
keeps a superseded request's last buffered chunk from touching the newer
wave's live label. Tile supersede remains client-abort only (no server
`KILL`) by design. Behavior byte-identical.
- **Workbench run lifecycle extracted into a route-scoped `WorkbenchSession`**
(#276 Phase 3a). `run`/`runScript`/`runEntry`/`cancel` orchestration now
lives in `src/ui/workbench/workbench-session.ts`; the run bookkeeping
Expand Down
772 changes: 168 additions & 604 deletions src/ui/dashboard.ts

Large diffs are not rendered by default.

676 changes: 676 additions & 0 deletions src/ui/dashboard/dashboard-session.ts

Large diffs are not rendered by default.

23 changes: 20 additions & 3 deletions src/ui/filter-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ const buildRecentField = _buildRecentField as (opts: {
// Enter/blur bypass this entirely for a fast explicit-commit path.
export const FILTER_DEBOUNCE_MS = 500;

/** `buildFilterBar`'s return value (#276 Phase 3b filter-bar dispose seam):
* `el` is the bar's root node; `dispose()` clears every field's pending
* debounce timer. A caller that rebuilds the bar (a filter-value merge
* repaint) must dispose the previous bar first — and dispose on its own
* teardown — so an in-flight debounce never fires against a detached field
* (the orphan-timer gap a bare `replaceChildren` rebuild used to leave). */
export interface FilterBarHandle {
el: HTMLElement;
dispose(): void;
}

/**
* Build a filter bar: one field per `{name:Type}` parameter in `params` (the
* shape from `fieldControls(analysis)`), sharing `app.state.varValues` /
Expand All @@ -107,20 +118,25 @@ export const FILTER_DEBOUNCE_MS = 500;
* the detached Data view passes its child-tab document so the comboboxes anchor
* in the right realm — #185). `options.ariaLabel`, when set, names the bar as a
* labeled group for assistive tech (the detached view labels it "Query filters").
*
* Returns `{ el, dispose }` (#276 Phase 3b) rather than the bare root node —
* see `FilterBarHandle`.
*/
export function buildFilterBar(
app: FilterBarApp,
params: FieldControl[],
onCommit: (name: string) => void,
getField: (name: string, mode: ValidationMode) => PreparedFieldState,
options: BuildFilterBarOptions = {},
): HTMLElement {
): FilterBarHandle {
const document = options.document || app.document;
const attrs: Record<string, unknown> = { class: 'dash-filters' };
if (options.ariaLabel) { attrs.role = 'group'; attrs['aria-label'] = options.ariaLabel; }
if (!params.length) return h('div', { ...attrs, style: { display: 'none' } });
return h('div', attrs, ...params.map((p) => {
if (!params.length) return { el: h('div', { ...attrs, style: { display: 'none' } }), dispose: () => {} };
const timerClears: Array<() => void> = [];
const el = h('div', attrs, ...params.map((p) => {
let timer: ReturnType<typeof setTimeout> | null = null;
timerClears.push(() => { if (timer != null) clearTimeout(timer); timer = null; });
// #173 acceptance (review F1): a type-conflicted param (declared with
// disagreeing types across favorites) degrades to the plain text control
// (fieldControlKind below) and says so visibly — a warning style distinct
Expand Down Expand Up @@ -221,4 +237,5 @@ export function buildFilterBar(
return h('label', { class: 'var-field' + (p.optional ? ' is-optional' : '') },
h('span', { class: 'var-name' }, p.name), combo.el);
}));
return { el, dispose: () => timerClears.forEach((clear) => clear()) };
}
5 changes: 4 additions & 1 deletion src/ui/results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,7 @@ export function expandDataPane(app: ResultsApp, r: QueryResult): DetachedView {
// Filter row (#185): only when the source declares `{name:Type}` fields —
// omitted entirely otherwise (no empty toolbar). Committing a field or
// clicking Refresh re-runs only this detached query.
let filterBarDispose: (() => void) | null = null;
if (fields.length) {
const getField = (name: string, mode: ValidationMode): PreparedFieldState => prepareParameterizedBatch(analysis, {
values: app.state.varValues,
Expand All @@ -1039,12 +1040,13 @@ export function expandDataPane(app: ResultsApp, r: QueryResult): DetachedView {
// A committed field re-runs only this detached query (rerun ignores the
// param name buildFilterBar passes — a single source re-runs wholesale).
const filterBar = buildFilterBar(app as FilterBarApp, fields, rerun, getField, { document: doc, ariaLabel: 'Query filters' });
filterBarDispose = filterBar.dispose;
refreshBtn = h('button', {
class: 'res-act detached-refresh', title: 'Re-run this query with the current filter values',
onclick: () => rerun(),
}, Icon.play(), h('span', null, 'Refresh'));
statusEl = h('div', { class: 'detached-status', role: 'status' });
pane.appendChild(h('div', { class: 'detached-filter-row' }, filterBar, refreshBtn, statusEl));
pane.appendChild(h('div', { class: 'detached-filter-row' }, filterBar.el, refreshBtn, statusEl));
}
pane.appendChild(inner);
body.appendChild(pane);
Expand All @@ -1066,6 +1068,7 @@ export function expandDataPane(app: ResultsApp, r: QueryResult): DetachedView {
closed = true;
if (ac) ac.abort();
if (chartInstance) { chartInstance.destroy(); chartInstance = null; }
filterBarDispose?.();
if (!isTab) doc.removeEventListener('keydown', onKey, true);
};
},
Expand Down
Loading
Loading