Skip to content

Dashboard query-backed filters: searchable multiselect with Apply #189

Description

@BorisTyshkevich

Dependencies

This issue depends on:

  1. Saved-query format v2: separate SQL from an extensible Spec document #211
  2. Workbench: add separate SQL and Spec JSON editor modes #212
  3. 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:

single
multiple

At dashboard.selection, suggest:

mode

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:

  1. opens or activates the saved query;
  2. switches to Spec mode;
  3. 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:

  1. maps the draft back to option values;
  2. deduplicates values;
  3. canonicalizes ordering;
  4. compares the canonical array with the committed array;
  5. writes shared value and activation only when changed;
  6. persists once;
  7. closes the popover;
  8. 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:

  1. derive available values;
  2. intersect the active committed array with available values;
  3. canonicalize according to new option order;
  4. 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

  • A Filter source can set dashboard.selection.mode:"multiple".
  • Omitted mode remains strict single-select.
  • Only compatible Array-typed targets are accepted.
  • Authoring occurs in Spec JSON, not a larger Library form.
  • Library/workbench badges identify multiselect sources.
  • Dashboard renders a searchable checklist with draft state.
  • Apply commits once and causes at most one affected-Panel wave.
  • Unchanged Apply causes no wave.
  • Cancel/Escape/outside dismissal persist nothing and issue no request.
  • Select visible affects only the filtered subset.
  • All/Not set remains distinct from active empty-string values.
  • Arrays serialize through the shared typed pipeline without loss.
  • Option refresh preserves the valid intersection and never auto-selects.
  • Stale open drafts cannot commit after option refresh.
  • Keyboard, focus, and accessible state are covered.
  • Unknown Spec fields are preserved.
  • Coverage gates, build, README, and CHANGELOG updates pass.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions