Dependencies
This issue depends on:
Saved-query format v2: separate SQL from an extensible Spec document #211
Workbench: add separate SQL and Spec JSON editor modes #212
Dashboard: query-backed filter options — single-select MVP #160
Authoring is performed in Spec JSON.
This issue does not add a Library configuration form.
Problem
#160 deliberately provides strict single-select query-backed filters.
Some ClickHouse parameters naturally bind arrays:
WHERE carrier IN {carrier:Array(String)}
A native <select multiple> is not acceptable:
poor search for long option lists;
unclear selected-state summary;
awkward keyboard behavior;
each click could trigger a dashboard query wave;
difficult All/Clear semantics;
no clean draft-versus-committed interaction.
Goal
Allow a query-backed Filter source to request multiselect behavior in its Spec:
{
"name" : " Carrier options" ,
"favorite" : true ,
"dashboard" : {
"role" : " filter" ,
"param" : " carrier" ,
"selection" : {
"mode" : " multiple"
}
}
}
Render a searchable checklist popover with local draft state and an explicit Apply action.
Single-select remains the default:
{
"dashboard" : {
"role" : " filter" ,
"param" : " carrier"
}
}
is equivalent to:
{
"dashboard" : {
"role" : " filter" ,
"param" : " carrier" ,
"selection" : {
"mode" : " single"
}
}
}
Why selection.mode instead of multiple: true
Use an explicit extensible namespace:
{
"selection" : {
"mode" : " single"
}
}
or:
{
"selection" : {
"mode" : " multiple"
}
}
This avoids accumulating unrelated Filter-source booleans directly under dashboard and leaves room for future selection behavior without changing the role model.
Unknown fields inside selection are preserved.
Unknown non-empty modes are diagnosed and fall back safely.
Because #189 is not implemented yet, no compatibility migration from the earlier draft multiple:true key is required.
Authoring UX
Spec JSON is authoritative
Users configure multiselect in the workbench Spec editor.
Do not add to the Library pencil form:
Multiple selection checkbox;
select-mode dropdown;
advanced Filter-source settings;
larger edit form.
The pencil remains Name/Description only.
The star remains favorite/inclusion only.
Spec completion
Inside a Filter source:
{
"dashboard" : {
"role" : " filter" ,
"param" : " carrier" ,
"selection" : {
"mode" : " "
}
}
}
suggest:
At dashboard.selection, suggest:
Completion assists but never blocks unknown extension fields.
Spec diagnostics
Provide path-specific diagnostics.
Examples:
dashboard.selection.mode
Expected "single" or "multiple".
dashboard.selection.mode
Multiselect requires every executable declaration of "carrier" to use a compatible Array type.
dashboard.param
Parameter "carrier" has conflicting scalar and Array declarations.
dashboard.param
Nested arrays are not supported by the current parameter serializer.
Editor validation assists authoring.
Runtime validation remains authoritative.
Library and workbench badges
A multiselect source should be distinguishable from a single-select source.
Examples:
★ Carriers [Filter · carrier · Multi]
★ Airports [Filter · origin]
The workbench role badge uses the same summary:
Carrier options Filter · carrier · Multi
Clicking the badge:
opens or activates the saved query;
switches to Spec mode;
focuses dashboard.selection or the relevant diagnostic.
Preconditions inherited from #160
#160 provides:
spec.dashboard.role;
spec.dashboard.param;
Filter-source SQL/result validation;
option-query execution;
strict single-select lifecycle;
option-source diagnostics;
shared values and activation;
lossless option strings;
server-side option cap;
cancellation and stale-response protection;
Library/workbench role badges;
Spec completion and diagnostics infrastructure.
This issue extends those contracts rather than creating a second Filter-source path.
Configuration validation
A multiselect Filter source is valid only when every executable dashboard declaration of its target parameter is compatible with an Array type.
Valid examples:
Array(String)
Array(UInt64)
Array(Int64)
Array(Decimal(...))
Array(Enum8(...))
Array(Date)
Array(DateTime)
Validity follows the shared typed serializer's actual supported scalar element types.
Invalid configurations:
scalar target with selection.mode:"multiple";
conflicting scalar and Array declarations;
conflicting Array element types;
nested arrays unsupported by the serializer;
target unused by executable dashboard Panels;
target used only by excluded or invalid Panels.
Invalid configuration:
does not execute the option source;
falls back to the ordinary parameter control;
shows a visible diagnostic;
leaves unrelated Panels and Filter sources functional.
Do not silently downgrade a requested multiple mode to single-select.
Saved Spec behavior
The complete Spec is preserved:
{
"dashboard" : {
"role" : " filter" ,
"param" : " carrier" ,
"selection" : {
"mode" : " multiple" ,
"x-extension" : {
"future" : true
}
}
}
}
Known-field edits must not strip:
unknown dashboard fields;
unknown selection fields;
unrelated Panel configuration;
other Spec extensions.
Changing role away from Filter retains dormant dashboard.param and dashboard.selection.
Changing back restores the previous multiselect configuration.
Stored value
Shared state stores the committed array:
state . varValues . carrier = [ 'AA' , 'UA' ] ;
state . filterActive . carrier = true ;
Rules:
committed values are arrays;
option values remain lossless strings;
activation remains independent from stored value;
an inactive filter may retain a dormant non-empty array;
persistence round-trips arrays;
selection order is UI state only and must not affect SQL meaning;
duplicates are removed before commit;
empty-string option values remain valid array elements.
Do not use an empty array as the sole inactive sentinel.
The authoritative active state remains:
state . filterActive [ param ]
Closed control
Examples:
Carrier: All
Carrier: Not set
Carrier: American Airlines
Carrier: 4 selected
The trigger shows:
Selected chips are optional and out of scope for the MVP.
Popover
Recommended structure:
Search carriers…
[ ] Select visible
[x] American Airlines
[ ] Delta Air Lines
[x] United Airlines
…
Clear Cancel Apply
Behavior:
search filters the complete loaded option list locally;
each option has a checkbox;
Select visible affects only options in the current filtered result;
Clear empties the draft;
Cancel discards the draft;
Escape behaves like Cancel;
clicking outside behaves like Cancel;
Apply commits once;
focus returns to the trigger after Apply, Cancel, Escape, or outside dismissal.
Do not force the single-select combobox primitive into multiselect ARIA roles.
Reuse lower-level filtering, positioning, and option-label helpers where sensible, but implement a dedicated multiselect controller.
Draft versus committed state
Opening the popover copies committed state into local draft state:
draft = new Set ( committedValues ) ;
Checkboxes, Clear, and Select visible modify only the draft.
They must not:
update shared stores;
persist values;
rerun Panels;
record recents.
Apply
Apply:
maps the draft back to option values;
deduplicates values;
canonicalizes ordering;
compares the canonical array with the committed array;
writes shared value and activation only when changed;
persists once;
closes the popover;
triggers at most one affected-Panel wave.
If the canonical committed value is unchanged, close without a query wave.
Activation after Apply:
non-empty draft → active;
empty draft → inactive;
dormant previous values are replaced by the committed empty selection only when the user explicitly applied Clear.
Use one wallNowMs for the entire affected wave.
Cancel paths
Cancel, Escape, and outside dismissal:
discard draft;
write nothing;
persist nothing;
issue no network request.
Canonical ordering
Selection order must not create false changes.
Canonicalize committed arrays according to the authoritative option-list order.
Example:
options: [AA, DL, UA]
draft clicks: UA, AA
committed: [AA, UA]
This ensures:
identical selections compare equal;
click order does not alter SQL meaning;
refresh reconciliation has deterministic output;
persistence remains stable.
Any previously committed value not present in the current option list is handled by refresh reconciliation before the control becomes usable.
Select visible
Select visible affects the current filtered subset only.
Behavior:
unchecked when no visible values are selected;
checked when every visible value is selected;
mixed when some visible values are selected;
activating it while unchecked/mixed adds all visible values;
activating it while checked removes all visible values;
hidden selected values remain unchanged.
Its accessible label must make filtered scope explicit:
Select all 12 visible options
or:
Clear all 12 visible options
Option refresh reconciliation
When #160 refreshes a Filter source:
derive available values;
intersect the active committed array with available values;
canonicalize according to new option order;
compare by bound values, not labels.
Rules:
non-empty intersection → persist it and remain active;
empty intersection → deactivate;
keep dormant value according to the shared inactive-value policy;
never select newly introduced options;
label-only changes do not rerun Panels;
order-only changes do not rerun Panels;
bound-value removal causing a changed selection triggers the normal reconciled Panel wave once.
If a multiselect popover is open when a newer option generation lands:
close it as Cancel before reconciliation, or
explicitly rebuild the draft from the reconciled committed value.
Preferred MVP: close it as Cancel and announce that options were refreshed.
Never apply a draft built against stale options.
All, Not set, and empty-string values
Inactive identity is not an option value.
An active empty-string option remains valid:
and is distinct from:
and from:
filterActive . carrier === false
Do not use '', [], or a DOM option value as an ambiguous sentinel.
Parameter serialization
Use the existing shared typed parameter pipeline.
The multiselect control supplies an array value. The serializer remains responsible for ClickHouse transport according to the declared Array type.
Cover:
quotes;
commas;
backslashes;
Unicode;
empty strings;
large integers;
Decimal values;
Enum labels;
Date/DateTime strings.
Do not build ClickHouse Array literals in the UI component.
Recent values
The Filter source is authoritative; do not append a recents section to the checklist.
Successful Panel executions may record arrays only when the shared recent-value model explicitly supports typed arrays.
Otherwise array recents remain out of scope.
Do not stringify arrays into ambiguous comma-separated recent values.
Loading and failure states
While options load:
trigger communicates Loading options…;
opening the checklist is disabled or shows a noninteractive loading body;
previously committed values remain stored.
On option-query failure:
On Retry success, reconcile normally.
Accessibility
Use a trigger/button plus dialog/checklist pattern appropriate for multiselection.
Requirements:
trigger accessible name includes parameter label;
trigger communicates selected count;
popover has dialog name;
search input is labeled;
options expose checkbox state;
mixed state is exposed for Select visible;
Apply, Cancel, and Clear are keyboard reachable;
Escape discards;
focus returns to trigger;
loading/error status is announced;
refreshed-options cancellation is announced;
keyboard navigation does not commit until Apply.
Network and concurrency
Checkbox interaction is local only.
Apply triggers one affected-Panel wave through the existing #160 /#193 dashboard execution path.
Requirements:
one generation reservation for the wave;
one wallNowMs;
shared concurrency limiter;
latest-request semantics;
stale results never land;
no option-source rerun on Apply;
no request per checkbox.
Files
Expected changes:
src/core/dashboard.js
selection-mode normalization;
Array compatibility validation;
array reconciliation and canonicalization.
Spec completion/diagnostic provider
dashboard.selection.mode.
src/ui/saved-history.js
badge summary only; no setting form.
src/ui/dashboard.js
trigger, checklist popover, draft controller, Apply.
shared option filtering/position helpers where reusable.
src/core/param-serialize.js
no change expected unless tests expose unsupported Array behavior.
src/styles.css
tests
README
CHANGELOG
No new runtime dependency.
Tests
Spec authoring
completion offers selection and single | multiple;
omitted selection defaults to single;
unknown mode is diagnosed;
no multiselect setting appears in the pencil editor;
badge shows Filter · carrier · Multi;
badge navigates to Spec;
unknown Spec fields survive.
Configuration
multiple mode with compatible Array target is valid;
scalar target is diagnostic;
conflicting scalar/Array declarations are diagnostic;
conflicting Array element declarations are diagnostic;
unsupported nested arrays are diagnostic;
unused target is invalid;
role changes preserve dormant selection configuration.
Draft UI
opening seeds draft from committed selection;
checkbox changes remain local;
search filters complete options;
Select visible affects filtered subset only;
mixed Select-visible state is correct;
Clear modifies draft only;
Cancel, outside click, and Escape discard;
Apply commits once;
unchanged Apply triggers no wave;
focus returns to trigger;
selected summary is correct.
Execution
Apply causes exactly one affected-Panel wave;
one wallNowMs is used;
no option request runs on Apply;
no request runs per checkbox;
Array values reach the shared serializer intact;
special characters and large numeric strings round-trip.
Reconciliation
refresh keeps valid intersection;
removed values are dropped;
empty intersection deactivates;
new options are never selected;
label-only changes do not rerun;
option-order changes do not rerun;
open stale draft is cancelled when options refresh;
active empty-string value remains distinct from inactive.
Accessibility
trigger name and selected count;
dialog and search labels;
checkbox checked states;
mixed Select-visible state;
Apply/Cancel keyboard access;
Escape discard;
focus restoration;
loading/error/refreshed status announcements.
Acceptance criteria
Non-goals
Native <select multiple>.
Cascading/dependent Filter sources.
Option-query caching.
Per-option grouping.
Drag-reordering selected values.
Selected-value chips.
Array recent history unless supported cleanly.
Form-based multiselect configuration.
Dependencies
This issue depends on:
Authoring is performed in Spec JSON.
This issue does not add a Library configuration form.
Problem
#160 deliberately provides strict single-select query-backed filters.
Some ClickHouse parameters naturally bind arrays:
A native
<select multiple>is not acceptable:Goal
Allow a query-backed Filter source to request multiselect behavior in its Spec:
{ "name": "Carrier options", "favorite": true, "dashboard": { "role": "filter", "param": "carrier", "selection": { "mode": "multiple" } } }Render a searchable checklist popover with local draft state and an explicit Apply action.
Single-select remains the default:
{ "dashboard": { "role": "filter", "param": "carrier" } }is equivalent to:
{ "dashboard": { "role": "filter", "param": "carrier", "selection": { "mode": "single" } } }Why
selection.modeinstead ofmultiple: trueUse an explicit extensible namespace:
{ "selection": { "mode": "single" } }or:
{ "selection": { "mode": "multiple" } }This avoids accumulating unrelated Filter-source booleans directly under
dashboardand leaves room for future selection behavior without changing the role model.Unknown fields inside
selectionare preserved.Unknown non-empty modes are diagnosed and fall back safely.
Because #189 is not implemented yet, no compatibility migration from the earlier draft
multiple:truekey is required.Authoring UX
Spec JSON is authoritative
Users configure multiselect in the workbench Spec editor.
Do not add to the Library pencil form:
The pencil remains Name/Description only.
The star remains favorite/inclusion only.
Spec completion
Inside a Filter source:
{ "dashboard": { "role": "filter", "param": "carrier", "selection": { "mode": "" } } }suggest:
At
dashboard.selection, suggest:Completion assists but never blocks unknown extension fields.
Spec diagnostics
Provide path-specific diagnostics.
Examples:
Editor validation assists authoring.
Runtime validation remains authoritative.
Library and workbench badges
A multiselect source should be distinguishable from a single-select source.
Examples:
The workbench role badge uses the same summary:
Clicking the badge:
dashboard.selectionor the relevant diagnostic.Preconditions inherited from #160
#160 provides:
spec.dashboard.role;spec.dashboard.param;This issue extends those contracts rather than creating a second Filter-source path.
Configuration validation
A multiselect Filter source is valid only when every executable dashboard declaration of its target parameter is compatible with an Array type.
Valid examples:
Validity follows the shared typed serializer's actual supported scalar element types.
Invalid configurations:
selection.mode:"multiple";Invalid configuration:
Do not silently downgrade a requested multiple mode to single-select.
Saved Spec behavior
The complete Spec is preserved:
{ "dashboard": { "role": "filter", "param": "carrier", "selection": { "mode": "multiple", "x-extension": { "future": true } } } }Known-field edits must not strip:
dashboardfields;selectionfields;Changing role away from Filter retains dormant
dashboard.paramanddashboard.selection.Changing back restores the previous multiselect configuration.
Stored value
Shared state stores the committed array:
Rules:
Do not use an empty array as the sole inactive sentinel.
The authoritative active state remains:
Closed control
Examples:
The trigger shows:
AllorNot setwhen inactive, using Dashboard: query-backed filter options — single-select MVP #160 requiredness rules;N selectedfor multiple values;Selected chips are optional and out of scope for the MVP.
Popover
Recommended structure:
Behavior:
Select visibleaffects only options in the current filtered result;Do not force the single-select combobox primitive into multiselect ARIA roles.
Reuse lower-level filtering, positioning, and option-label helpers where sensible, but implement a dedicated multiselect controller.
Draft versus committed state
Opening the popover copies committed state into local draft state:
Checkboxes, Clear, and Select visible modify only the draft.
They must not:
Apply
Apply:
If the canonical committed value is unchanged, close without a query wave.
Activation after Apply:
Use one
wallNowMsfor the entire affected wave.Cancel paths
Cancel, Escape, and outside dismissal:
Canonical ordering
Selection order must not create false changes.
Canonicalize committed arrays according to the authoritative option-list order.
Example:
This ensures:
Any previously committed value not present in the current option list is handled by refresh reconciliation before the control becomes usable.
Select visible
Select visibleaffects the current filtered subset only.Behavior:
Its accessible label must make filtered scope explicit:
or:
Option refresh reconciliation
When #160 refreshes a Filter source:
Rules:
If a multiselect popover is open when a newer option generation lands:
Preferred MVP: close it as Cancel and announce that options were refreshed.
Never apply a draft built against stale options.
All, Not set, and empty-string values
Inactive identity is not an option value.
An active empty-string option remains valid:
and is distinct from:
and from:
Do not use
'',[], or a DOM option value as an ambiguous sentinel.Parameter serialization
Use the existing shared typed parameter pipeline.
The multiselect control supplies an array value. The serializer remains responsible for ClickHouse transport according to the declared Array type.
Cover:
Do not build ClickHouse Array literals in the UI component.
Recent values
The Filter source is authoritative; do not append a recents section to the checklist.
Successful Panel executions may record arrays only when the shared recent-value model explicitly supports typed arrays.
Otherwise array recents remain out of scope.
Do not stringify arrays into ambiguous comma-separated recent values.
Loading and failure states
While options load:
Loading options…;On option-query failure:
On Retry success, reconcile normally.
Accessibility
Use a trigger/button plus dialog/checklist pattern appropriate for multiselection.
Requirements:
Network and concurrency
Checkbox interaction is local only.
Apply triggers one affected-Panel wave through the existing #160/#193 dashboard execution path.
Requirements:
wallNowMs;Files
Expected changes:
src/core/dashboard.jsdashboard.selection.mode.src/ui/saved-history.jssrc/ui/dashboard.jssrc/core/param-serialize.jssrc/styles.cssNo new runtime dependency.
Tests
Spec authoring
selectionandsingle | multiple;Filter · carrier · Multi;Configuration
Draft UI
Execution
wallNowMsis used;Reconciliation
Accessibility
Acceptance criteria
dashboard.selection.mode:"multiple".Non-goals
<select multiple>.