Skip to content

feat(#360): parameterized Dashboard Filter sources with single-layer dependencies#362

Merged
BorisTyshkevich merged 16 commits into
mainfrom
feat/filter-source-params-360
Jul 21, 2026
Merged

feat(#360): parameterized Dashboard Filter sources with single-layer dependencies#362
BorisTyshkevich merged 16 commits into
mainfrom
feat/filter-source-params-360

Conversation

@BorisTyshkevich

@BorisTyshkevich BorisTyshkevich commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

What & why

Closes #360.

Lets a Filter-role source query declare its own {name:Type} parameters and bind
committed root Dashboard filter values through the existing native
param_<name> pipeline (no textual SQL interpolation) — so a query-log option
source can compute its user / query-kind / exception-code lists for the selected
{from:DateTime} / {to:DateTime} window instead of a hard-coded range.
Single-layer only: a Filter source may depend on root filters, never on another
source-backed filter.

Builds directly on #359's one-FilterSourceRuntime-per-source boundary (merged).

Behavior

  • A source waits (issues no ClickHouse request) until every required root
    parameter is committed, active, valid, and serializable — exposing a non-error
    waiting state that names the missing params.
  • Relative values (-1d, now) resolve against one wall clock per wave.
  • Native typed param_<name> args only — no textual interpolation.
  • Changing a root filter reruns only the sources that declare it, then the
    affected panels, in one combined wave; a refreshed option set that drops a
    filter's active value deactivates it before panels run.
  • A Filter source depending on another source-backed parameter is rejected
    with a visible filter-source-cascading diagnostic.
  • A superseded source response can never publish stale results; a dependency
    change clears the affected options so a stale set can't appear current while
    the new source loads (no silent last-known-good).
  • Dashboard execution and the Workbench Filter preview share one preparation
    op
    (prepareFilterSource) — identical structural diagnostics, relative-value
    resolution, optional-block materialization, validation/serialization, and args.
  • Filter execution injects no readonly setting (kept from Refactor Dashboard Filter sources: execute once per source, preserve diagnostics, and reconcile option state #359).

Shape

  • src/core/filter-execution.ts — new pure analyzeFilterSource +
    prepareFilterSource (the shared op) wrapping the existing param pipeline;
    the old filter-source-parameters diagnostic is removed. 100% covered.
  • src/dashboard/application/dashboard-viewer-session.tsFilterSourceRuntime
    gains analyzed/waiting; selective source rerun + combined panel wave;
    cross-source mid-flight guard; single preflight per commit; stale-options
    clear on the selective path. (Also removes a stray NUL byte from refactor(#359): one FilterSourceRuntime per unique Filter source query #361.)
  • src/ui/{dashboard,filter-bar,filter-preview}.ts + styles.css — waiting /
    stale / error affordance for source-backed filters.
  • src/application/workbench-parameter-session.ts + src/ui/workbench/workbench-session.ts
    — Workbench Filter preview uses the shared op via a prepareFilterPreview hook.
  • examples/query-log-explorer.jsonqle-filter migrated to a half-open
    {from:DateTime}/{to:DateTime} window (worked example).

Verification

Checklist

  • npm test passes (the per-file coverage gate is non-negotiable)
  • Tests added/updated in the same change as the code
  • npm run build succeeds (single-file dist/sql.html)
  • Layers kept honest: pure logic in src/core/, network in src/net/ (injected fetch), DOM in src/ui/
  • No new runtime dependency
  • README / CHANGELOG.md ([Unreleased]) updated (behavior changed)
  • Reconciled affected tracked work (roadmap Roadmap to 1.0.0 #68, the issue body, CHANGELOG)

Review round (maintainer feedback on the merge-gate build)

Addressed all findings from the merge-gate review, as commits on this PR:

  • Superseded/destroyed selective commit no longer runs its panel wave — a
    superseded source wave (per-source generation) or a destroy() now stops the
    affected-panel phase (findings 1+2). applyFilterProviders/runFilterSourceWave
    return applied | superseded; runAffectedWave got an unconditional destroyed
    guard. (A first attempt used a commit-level generation counter; a follow-up
    review showed it over-skipped legitimate unrelated overlapping commits, so it
    was removed — the per-source supersession check is sufficient and precise.)
  • Param type-conflicts are errors, not runnable (finding 3) — analyzeFilterSource
    folds the pipeline's type-conflict diagnostics; a conflicted source errors on
    both surfaces, no request.
  • Source-backed filters render by topology, not transient status (major UI) —
    curated renderer chosen by explicit sourceId; loading/stale/waiting applied
    imperatively (no rebuild, typing preserved), so the affordance shows on initial
    load and on a full refresh with existing options.
  • Example consistencyqle-filter's {to} is now optional/inclusive,
    matching its panels.
  • DeduprunFilterWave/runFilterSourceWave share executeFilterSourcePlan;
    prepareFilterSource no longer re-runs structural diagnostics for transport params.

Re-verified: npm test (4482), npm run build, e2e chromium+webkit (154), plus a
second independent review pass.

BorisTyshkevich and others added 9 commits July 21, 2026 14:32
…terSource (core)

Filter-role source SQL may now declare its own {name:Type} query parameters:
the filter-source-parameters diagnostic is removed from filterSqlDiagnostics
(structural rules — empty / statement-count / row-returning / owned-FORMAT —
stay). Two new pure functions wrap the shared param-pipeline so both the
Dashboard viewer and the Workbench Filter preview prepare a source identically
(no independently-implemented binding):

- analyzeFilterSource(sql, {sourceBackedParams, label}) -> {sql, analysis,
  dependsOn, diagnostics}: structural + single-layer cascading diagnostics
  (a dep backed by ANOTHER Filter source -> filter-source-cascading) and the
  declared param names, derived once per SQL edit.
- prepareFilterSource(analyzed, {values, active, wallNowMs}) -> readiness
  ('runnable'|'waiting'|'error') + owned transport params (caps only, no
  readonly) merged with native param_<name> args + materialized execSql +
  bound-param snapshots. missing -> waiting; invalid/errors/diagnostics ->
  error; else runnable. validationMode is always 'execute'.

Foundation for #360 (Wave 1). Dashboard runtime + Workbench parity follow.
filter-execution.ts stays 100/100/100/100; full gate + build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Dashboard viewer now runs parameterized Filter-role source queries fed by
committed root filter values (native param_<name>), with single-layer
dependency scheduling on top of #359's one-runtime-per-source boundary.

- FilterSourceRuntime gains `analyzed` (analyzeFilterSource at construction,
  with the dashboard's source-backed param set for cascading rejection) and a
  `waiting` status; each wave prepares the source via prepareFilterSource
  against committedRootValues() (inactive root filters blanked so they gate)
  and one deps.wallNow() reading per wave (relative {from}/{to} share one clock).
- A source issues NO request while waiting (missing deps) or errored
  (invalid value / structural / cascading); runnable sources execute the
  materialized execSql with the bound args, then recordBoundParams for parity.
- Selective rerun: committing a root filter reruns ONLY the sources whose SQL
  declares it (runFilterSourceWave), then folds the changed param plus any
  reconciliation-deactivated names into ONE combined affected-panel wave
  (commitAndRerun); the selective wave preflights its own token.
- applyFilterProviders returns the flipped (deactivated) names and, per the
  plan review, skips re-deriving a mid-flight non-plan source's consumers so an
  overlapping selective wave can't flip a still-loading source back to stale
  options. Published per-filter view gains optional `stale`/`waitingFor`.
- Fixes the stray NUL byte in optionsSignature (from #361).

Wave 2a of #360. Filter-bar waiting/stale UI (2b) + Workbench parity (3) follow.
dashboard-viewer-session.ts: 100% stmts/fns/lines, 94.75% branches (gate ≥90).
Full gate + build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed filters

A source-backed Dashboard filter with no options (waiting on a root dependency,
mid-flight, or errored) used to fall out of the curated rendering path and
render as a bare, unlabelled plain field — the plan-review BLOCKER-2. Now:

- rebuildFilterBar gates the curated path on `f.status !== 'idle'` (a plain
  filter stays 'idle' forever), forwarding status/stale/waitingFor, so a
  source-backed filter stays curated regardless of whether it has options yet.
- The barSig rebuild trigger gains a sticky-status term (waiting + the three
  error statuses + waitingFor) so an idle→waiting transition with unchanged
  options actually repaints; bare 'loading' is excluded to preserve #359's
  same-content-refresh-never-rebuilds invariant.
- filter-bar renders a STRUCTURAL, test-observable affordance (happy-dom sees
  no CSS): is-waiting (disabled + "Waiting for: <names>" text), is-error
  (disabled), is-stale (disabled, last options kept but marked not-current).
- filter-option-field.ts is untouched (affordance applied onto the input it
  already returns) — zero cross-wave contract risk.

Wave 2b of #360. filter-bar.ts 100% (branches 97.18); dashboard.ts floor held.
Full gate + build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… ones can't appear current

Per the issue's "Error and stale-result behavior": when a dependency changes,
prior options must not continue to appear current while the new source loads.
runFilterSourceWave (the dependency-changed path only) now clears each affected
consumer's options (setConsumerOptions null) when marking it loading, so the
filter-bar repaints the is-stale/loading affordance instead of showing the old
options as fresh; applyFilterProviders repopulates from the settled merge.
runFilterWave (full refresh) is untouched, preserving #359's
no-flicker-on-same-content invariant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rce (parity)

The Workbench Filter-tab preview now prepares its source through the exact same
analyzeFilterSource/prepareFilterSource pipeline the Dashboard uses, instead of
the old standalone filterExecution() — so structural diagnostics, relative-value
resolution (one wave clock), optional-block materialization, validation,
serialization, and native param_<name> args are identical on both surfaces
(the issue's "do not independently implement parameter binding").

- WorkbenchParameterSession.prepareFilterPreview(sql, wallNowMs) wraps the shared
  op with the var-strip's values/active (no sourceBackedParams — the Workbench
  has no Dashboard topology), mirroring prepareTabSource; wired through app.ts's
  createWorkbenchSession hooks and the fake-app test stub.
- run()'s Filter branch: error/waiting return before any request; runnable
  executes execSql with the bound params and records boundParams. The generic
  var-gate/toast stays bypassed for Filter tabs (kept the !isFilter guard) so a
  missing param yields the non-error 'waiting' preview, not a toast-block.
- filter-preview gains a 'waiting' state ("Waiting for: <names>"). The
  role-agnostic variable strip still surfaces the tab's {from:DateTime} controls
  — the intended way to type test values.

Wave 3 of #360. workbench-parameter-session.ts + filter-preview.ts 100%;
workbench-session.ts 100% stmts/fns/lines. Full gate + build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aves (review)

Code review found commitAndRerun double-preflighted: when a committed root param
feeds a Filter source, both runFilterSourceWave and runAffectedWave called
preflight() — so ensureFreshToken ran twice per commit and, on a stale token,
onAuthFailed fired twice (double toast/redirect). Now the affected path
preflights exactly once in commitAndRerun; runFilterSourceWave (only ever
reached from that path) no longer preflights, and runAffectedWave keeps its own
guard (it is still reached directly on the no-affected-source fast path, whose
timing is unchanged). The regression test now asserts ensureFreshToken and
onAuthFailed fire exactly once for one affected commit; added a test that
clearFilter on a dependency root re-gates the dependent source to 'waiting'
(the retained value is blanked, not stale-executed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e filter affordance (review)

- workbench-session run(): a Filter tab prepares via the shared filterPrep, so
  it no longer also runs the generic prepareTabSource over the same SQL (`src`
  is null for Filter tabs — it was only ever read on the non-Filter path).
- styles.css: distinguish the source-backed filter transport states so a
  disabled field's reason reads at a glance — is-error (red), is-waiting
  (dashed + "Waiting for: …" note), is-stale (dimmed/italic while reloading);
  plus the waiting filter-preview message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cile)

- examples/query-log-explorer.json: the qle-filter source now scopes its user /
  query_kind option lists to the selected half-open {from:DateTime}/{to:DateTime}
  window (matching the panels, which already use from/to); from/to default to
  -1d/now active so the whole example runs out-of-box and demonstrates the
  waiting→ready transition when the range changes. Re-normalized.
- CHANGELOG [Unreleased]: document #360 (Added) and the stray-NUL fix (Fixed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…th native params

Extends the raw filter-source harness with a parameterized source (`geo`)
depending on two root filters (from/to). Asserts, in a real browser
(chromium + webkit): both deps inactive → status 'waiting', waitingFor
[from, to], zero requests; committing from+to → status 'ready', exactly one
executeRead, params carry param_from/param_to (no textual interpolation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@BorisTyshkevich BorisTyshkevich mentioned this pull request Jul 21, 2026
75 tasks
BorisTyshkevich and others added 7 commits July 21, 2026 16:51
…e (review)

analyzeFilterSource now folds the shared pipeline's own analysis.diagnostics
(param type-conflict, e.g. {x:UInt8} vs {x:String}) into its diagnostics as
filter-source-param-type-conflict — so prepareFilterSource classifies such a
source 'error' (no request) instead of runnable, restoring parity with the
generic param pipeline on both Dashboard and Workbench. Also extract
filterOwnedParams() so prepareFilterSource no longer calls filterExecution just
to recover the transport caps (which re-ran the structural diagnostics).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…its panel wave (review)

Findings 1+2: commitAndRerun ran the affected-panel wave unconditionally after
the source wave, so a commit superseded by a newer one (or by destroy()) still
issued panel requests and could bind stale source-backed values. Now:
- a commit-level generation guards both phases: after the source wave,
  commitAndRerun bails (no panel wave) if destroyed, if a newer commit bumped
  the generation, or if the source wave reports 'superseded' — flushing any
  genuinely-applied source merge via publish() first (a superseded result
  mutated nothing, so that's a harmless replay);
- applyFilterProviders / runFilterSourceWave now return a discriminated
  { status:'applied', flipped } | { status:'superseded' } (an empty array
  couldn't distinguish stale from applied-nothing-flipped);
- runAffectedWave gets an unconditional destroyed guard (the preflighted:true
  fast path previously had none).

Finding 6: extract executeFilterSourcePlan shared by runFilterWave (keep
options, reset diagnostics) and runFilterSourceWave (clear options, keep
diagnostics), removing the duplicated clock/plan/supersede/loading/publish/pool/
merge scaffolding.

Also publishes explicit ViewerFilterState.sourceId so the filter bar can choose
the curated renderer by topology, not transient status (UI fix follows).

dashboard-viewer-session.ts: 100% stmts/fns/lines, 94.24% branches. Gate green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…peratively (review)

Maintainer review: a source-backed filter's curated identity was inferred from
transient status (`f.status !== 'idle'`), so it rendered as a plain text control
on initial load, and the loading/stale affordance never showed on a full refresh
with existing options (barSig excludes 'loading'). Status is execution state, not
topology.

- rebuildFilterBar now gates the curated renderer on the explicit
  `ViewerFilterState.sourceId` (set once at construction) — a source-backed
  filter is curated at every status, including its initial 'idle'.
- buildFilterBar returns `updateStatus(states)`; each curated field keeps a
  handle and a shared applyFieldStatus() maps status → affordance
  (ready=enabled; waiting=disabled+is-waiting+note; error states=disabled+
  is-error; loading/idle/stale=disabled+is-stale). Applied at build time and on
  status-only changes WITHOUT rebuilding, so the same input persists and
  in-progress typing survives.
- The render effect splits its signature: barSig is now purely structural
  ([id, active, value, optionsRev, sourceId!=null]); a separate statusSig drives
  the imperative updateStatus. Preserves #359's no-flicker-on-same-content
  invariant while actually showing loading/stale/waiting.

filter-bar.ts 100/100/100 (97.53 branches); dashboard.ts floor held. Gate green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nels (review)

Maintainer review: the migrated source made both from and to required, but the
dashboard's panels treat to as optional (/*[ ... <= {to} ]*/, blank = up to now).
Align the source to the same contract — from required, to optional and inclusive
— so clearing to no longer forces the source into waiting while the panels keep
running. CHANGELOG wording updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e supersession (review round 2)

The commit-level generation counter (added last round) was bumped by EVERY
commit — including unrelated fast-path ones — so an unrelated overlapping commit
(e.g. Tab-then-Enter across two fields) made a legitimate, non-superseded commit
skip its affected-panel wave, leaving its tile silently bound to the old value.

The per-source generation check in applyFilterProviders (surfaced as
result.status === 'superseded') already covers every real supersession — the
same-param overlap (the newer commit supersede()s the shared source), a
concurrent full refresh() (runFilterWave supersedes all sources), and destroy()
— without penalizing two commits that touch genuinely unrelated sources. So
commitAndRerun now guards only on `destroyed || result.status === 'superseded'`;
the commitGeneration counter and its publish() workaround are removed. Added a
regression test: two unrelated overlapping commits each run their own panel wave.

dashboard-viewer-session.ts: 100% stmts/fns/lines, 94.21% branches. Gate green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…as superseded (review round 3)

runFilterWave discarded its SourceWaveResult, claiming a full refresh has no
later phase — but refresh() runs the affected-panel wave after the filter wave.
So a refresh whose Filter wave was superseded by a concurrent selective commit
still ran its affected panels before the superseding wave's source updates +
reconciliation, unprotected by tile generations (the selective commit reserves
its own tile generations only later, inside its runAffectedWave). runFilterWave
now returns SourceWaveResult, and refresh() skips its affected-panel wave when
that result is 'superseded' — the superseding selective commit is the current
source-of-truth and runs those panels after it settles. Regression test added
(verified it fails without the guard).

dashboard-viewer-session.ts: 100% stmts/fns/lines, 93.6% branches. Gate green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Maintainer review: several production comments documented review rounds,
"finding N", and abandoned approaches rather than stable invariants — notably
commitAndRerun's ~30-line description of the reverted commit-generation counter.
Condensed the #360-introduced review narrative to present-tense invariants that
link #360, leaving the review history in GitHub. commitAndRerun's doc comment:
49→17 lines. Comment-only (build byte-identical); comments belonging to other
issues (#359/#170/#173/#276/#291/#321/#344/#347) left untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@BorisTyshkevich
BorisTyshkevich merged commit 8920add into main Jul 21, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow Filter-role source queries to use root Dashboard parameters (single-layer dependencies)

1 participant