Skip to content

Make the detached Data view interactive: Panel switcher + query filters #185

Description

@BorisTyshkevich

Problem

The Data Pane Expand action opens the current result in a separate browser tab, with an in-app overlay fallback.

Today that detached view is intentionally a frozen snapshot:

  • it can switch between result representations;
  • it can sort, resize columns, inspect cells, and copy data;
  • it does not retain the source query as an independently runnable surface;
  • it has no {name:Type} filter controls;
  • changing a parameter in the workbench does not update it.

That is useful for inspecting a result, but it is not sufficient as a full-screen analytical panel. A user who expands a filtered query should be able to change its filters and inspect the refreshed result without returning to the SQL editor.

The detached view must also track the Panels work: after #166, the result switcher is Table | JSON | Panel, not a detached copy of the old Chart-only behavior.

Goal

Turn the detached Data view into a self-contained, read-only query surface:

Query · system.text_log
Warnings emitted by ClickHouse and authentication services
┌──────────────────────────────────────────────────────────────────────┐
│ Table | JSON | Panel                     12 rows              Copy   │
├──────────────────────────────────────────────────────────────────────┤
│ level: [Warning ▾]   from: [-7d]   to: [2026-07-10]   Refresh       │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│                          current result                              │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

Requirements:

  1. Preserve the full result-view switcher: Table | JSON | Panel.
  2. Show the query's parameter controls when its source SQL declares {name:Type} fields.
  3. Re-run only the detached view when a filter is committed.
  4. Use the same persisted filter values, optional-filter activation, and recent-value history as the SQL Browser and dashboards.
  5. Keep the main workbench result, active view, sorting, and panel state unchanged.
  6. Continue supporting both a real child tab and the existing overlay fallback.

Product semantics

Open behavior

Opening the detached view must be immediate:

  • render the current result snapshot first;
  • do not issue another request merely because Expand was clicked;
  • capture the exact source SQL that produced the result;
  • capture the saved-query title and description associated with that run;
  • capture the originating tab/session identity;
  • clone the source panel config into detached-local state.

The detached view remains bound to that captured source. Later edits or runs in the SQL editor do not replace its SQL or result.

View switcher

The switcher is local to the detached view:

Table | JSON | Panel
  • Table uses detached-local sort and column widths.
  • JSON renders the detached view's current rows.
  • Panel renders through the shared panel registry introduced by Panels: visualization registry + Panel drawer tab + Library panel field #166.
  • Switching views never runs SQL.
  • Panel configuration is read-only in this surface.
  • Panel config changes made later in the workbench do not live-sync into an already-open detached view.

Do not maintain a separate detached-only list of panel types. The detached surface must dispatch through the same panel registry as the workbench and dashboard.

Filter controls

Analyze the captured source SQL through the shared parameter pipeline.

Render the same field families already supported elsewhere:

  • text;
  • declared Enum;
  • schema-inferred Enum suggestions;
  • relative time;
  • recent values;
  • optional-filter activation;
  • validation and type-conflict states.

Only fields referenced by this one source are shown.

When the source has no parameters, omit the filter row entirely—no empty toolbar or spacing.

Shared filter state

Treat the detached view as a full-screen preview of the same query/panel used by the SQL Browser and dashboards.

Its filter controls read and write the existing shared stores:

app.state.varValues
app.state.filterActive
app.state.varRecent

Use the same persistence paths and keys as the other two surfaces:

  • asb:varValues;
  • asb:filterActive;
  • asb:varRecent;
  • asb:varRecentDisabled.

A value entered in any of the three surfaces must be the value offered by the other surfaces. The detached view must not introduce a second value map, detached-only activation state, or detached-only recent-value history.

On every committed edit:

  • update app.state.varValues[name];
  • update app.state.filterActive[name] using the existing optional-filter rules;
  • persist both stores immediately through the same helpers used by the SQL Browser/dashboard controls;
  • repaint the detached field from the shared state.

Changing a shared value in the detached view does not automatically re-run the SQL Browser or dashboard. It re-runs only this detached query. Other surfaces use the updated value on their next explicit run, refresh, or relevant control repaint.

When the detached surface and another surface share the same application instance, their controls should reflect the shared in-memory value. Separate browser application tabs share through browser storage. If live cross-tab control repaint is already supported by the common storage layer, preserve it; introducing a new detached-only synchronization mechanism is unnecessary.

Recent values

A successful detached run records bound values through the exact same app.recordBoundParams(...) path as SQL Browser runs and dashboard tile runs.

Record only after the winning request completes successfully:

  • never on validation failure;
  • never on request error;
  • never for an aborted or stale superseded response;
  • never for parameters confined to inactive optional blocks;
  • respect varRecentDisabled;
  • preserve the existing MRU/deduplication/cap behavior.

The detached view reads suggestions from the same recent-value store and writes successful bound values back to that store. This is shared browser behavior, not detached-view-local history.

Commit and refresh behavior

A committed filter change re-runs only the detached query:

  • Enter and blur commit immediately;
  • dropdown, enum, and relative-time selections commit immediately;
  • free-text typing uses the existing dashboard-style debounce;
  • a visible Refresh button re-runs with the current local values;
  • switching Table/JSON/Panel never re-runs.

Before a request:

  • prepare the source using one wall-clock snapshot;
  • apply optional-block activation;
  • validate and serialize typed parameters;
  • block the request when required values are missing or invalid;
  • show field-level validation in the filter row.

A blocked detached run keeps the previous result visible and displays a concise status such as:

Enter a value for: from

Result replacement

A successful detached run replaces only the detached result and updates:

  • rows and columns;
  • elapsed time;
  • bytes/rows metadata;
  • capped state;
  • the Table, JSON, and Panel renderers.

The main tab's tab.result, resultView, resultSort, panel config, and history are untouched.

Copy always copies the detached view's current result, not the original expand-time snapshot.

Request concurrency and cancellation

Each detached Data view owns:

AbortController
generation
running
  • A newer filter run supersedes an older one.
  • Abort the previous detached request when practical.
  • Ignore any response whose generation is stale.
  • Disable Refresh while that detached view's request is active.
  • Closing the tab/overlay aborts its request and destroys any live panel/chart renderer.
  • A detached request must not toggle the workbench's global state.running.

Multiple detached views may exist independently.

Source-query identity

expandDataPane(app, result) currently receives only a rendered result snapshot. Interactive filters require the exact query template and execution context.

Record execution metadata on normal row-returning results:

result.source = {
  sql,          // exact authored single-statement template used for this run
  tabId,        // originating tab
  rowLimit,     // cap used by the run
  title,        // query title captured at run time
  description,  // query description captured at run time, or ''
}

Rules:

  • For a normal whole-tab single-statement run, sql is that statement's authored template.
  • For Run selection, sql is the selected statement text captured at run time.
  • Optional-block markers remain in source.sql; the parameter pipeline derives the execution view on every detached refresh.
  • Explicit raw FORMAT results, EXPLAIN results, script summaries, errors without a structured result, and empty results remain ineligible for Data Pane Expand.
  • Do not reconstruct source SQL later from the active editor; it may have changed or another tab may be active.
  • Do not resolve title or description from the current Library at expand time; the saved entry may have been renamed, edited, deleted, or another tab may be active.
  • For a saved query, capture its saved name and description.
  • For an unsaved query, use the current tab name as the title.
  • If the title is blank or still the default Untitled, derive a stable fallback with the existing query-name helper.
  • Missing descriptions normalize to an empty string.

The panel config is cloned separately at expand time because it is presentation state, not query execution metadata.

Execution seam

Do not call the normal workbench run() action from the detached view. It mutates the active tab, global running state, history, result view, and editor-coupled state.

Add a dedicated app/controller seam, for example:

app.runDetachedData({
  source,
  values,       // snapshot of shared app.state.varValues for this wave
  active,       // snapshot of shared effective activation for this wave
  signal,
  queryId,
})
// -> { columns, rows, progress, capped, error }

It should reuse the same lower-level behavior as a normal structured SELECT:

  • fresh-token check;
  • parameter analysis/preparation;
  • optional-block execution view;
  • native ClickHouse param_* bindings;
  • server-side result cap;
  • streaming/parser logic;
  • total error shape.

It must not:

  • write tab.result;
  • write query history;
  • change state.resultView;
  • change state.resultSort;
  • change state.running;
  • trigger schema refresh.

After a successful winning response, the detached controller must call the existing shared recent-value recorder with that prepared source's boundParams. The lower-level request seam should return or expose those immutable bound-parameter snapshots rather than re-reading mutable global values after the request.

Session behavior

The detached rerun must preserve the originating tab's ClickHouse session when that source depends on session state such as temporary tables or SET.

Do not read app.activeTab() at refresh time. Resolve the captured source.tabId.

Because ClickHouse serializes requests within one session, prevent detached and workbench requests from racing on the same session:

  • reuse the existing session-lock retry policy, or
  • introduce a small per-session request queue shared by workbench and detached reads.

A detached query must never silently drop the originating session and produce a different result.

If the source tab no longer exists, its captured session identifier may still be used for the lifetime of the detached view; otherwise show a clear error rather than switching to another tab's session.

UI layout

Detached title and description

Replace the generic Data heading with the captured query identity.

Title

Render the title as the primary heading:

Query · system.text_log

Requirements:

  • use the captured result.source.title;
  • preserve the user's capitalization and punctuation;
  • render as plain text, never HTML;
  • single line on desktop;
  • ellipsize visually when necessary;
  • keep the full title in the title attribute;
  • use an actual heading element;
  • set the child browser tab's document.title to the same value.

Description

Render the captured description directly below the title:

Warnings emitted by ClickHouse and authentication services

Requirements:

  • show it only when non-empty after trimming;
  • preserve meaningful internal whitespace while collapsing accidental line breaks for the header;
  • render as plain text, never Markdown or HTML in this surface;
  • use a smaller, muted style;
  • allow up to two lines;
  • clamp overflow with an ellipsis;
  • expose the full description through the title attribute;
  • do not reserve vertical space when no description exists.

Header structure

The detached title bar should be structured as:

[ title                                      ] [ close, overlay only ]
[ description                                ]

The view switcher, result statistics, filters, Refresh, and Copy remain in the result toolbars below it. Do not place the query description into the same compact row as those controls.

Top toolbar

Below the title/description header, keep the compact first result row:

Table | JSON | Panel        rows / elapsed / capped        Copy

The browser tab itself supplies closing chrome. The overlay fallback retains its close button in the detached title bar.

Filter row

Add a second row below the view toolbar:

level: [Warning]   from: [-7d]   to: [2026-07-10]       Refresh

Behavior:

  • sticky with the top toolbar while result content scrolls;
  • wraps cleanly on narrower widths;
  • field labels and controls use the shared parameter-control styles;
  • Refresh remains aligned to the end when space permits;
  • no horizontal page overflow;
  • hidden completely when there are no fields.

The result body consumes the remaining height.

Accessibility

  • The view switcher uses the existing accessible tab/button semantics.
  • The filter row has an accessible label such as Query filters.
  • Refresh exposes its running/disabled state.
  • Validation is available through visible text and accessible descriptions.
  • Keyboard focus remains in the edited field across local repaint where possible.

Shared-control extraction

Do not copy the dashboard's private buildFilterBar() or the workbench's variable-strip field construction into results.js.

Extract a reusable parameter-control builder that supports:

buildParamControls({
  app,
  analysis,
  getField,
  getValue,
  setValue,
  getActive,
  setActive,
  onCommit,
  document,
  mode,
})

Expected consumers:

  • workbench variable strip;
  • dashboard global filters;
  • detached Data view.

Surface-specific layout wrappers may remain separate, but type selection, validation state, optional behavior, enum/relative/recent controls, and commit wiring must have one implementation.

If extracting all three consumers in one PR is too disruptive, at minimum extract the field-level builder first and keep only the row composition surface-specific.

Files

Likely changes:

  • src/ui/results.js
    • detached controller;
    • Table/JSON/Panel switcher;
    • filter row;
    • local rerun lifecycle.
  • src/ui/app.js
    • capture result source metadata;
    • add isolated detached-run seam.
  • src/ui/param-controls.js or equivalent
    • shared parameter field builder.
  • src/ui/dashboard.js
    • consume the shared field builder instead of private duplicated construction.
  • workbench variable-strip code
    • consume the shared field builder where practical.
  • src/core/param-pipeline.js
    • no new semantics expected; use the existing analyze/prepare API.
  • src/styles.css
    • detached filter row and sticky/full-height layout.
  • tests for results, app/controller, parameter controls, and detached fallback.

Tests

Source capture

  • Whole-tab single statement captures its exact authored source.
  • Saved-query runs capture the saved title and description used for that run.
  • Unsaved runs capture the tab title and derive a fallback when it is blank or Untitled.
  • Renaming or deleting the saved query after the run does not change an already-open detached view.
  • Title and description render as text and cannot inject markup.
  • Run selection captures the selected SQL, not the whole editor.
  • Changing editor text after the run does not change detached refresh SQL.
  • Switching active tabs does not change detached source/session identity.
  • Ineligible result kinds do not expose Expand.

View switcher

  • Detached view renders Table, JSON, and Panel.
  • Switching views performs zero network requests.
  • View state is local and does not change the workbench result view.
  • Panel rendering uses the shared registry and a cloned read-only config.
  • Destroy hooks run on switch and close.

Filters

  • No parameter declarations: no filter row.
  • Required and optional parameters render correctly.
  • Enum, inferred Enum, relative-time, and recent-value controls work.
  • Missing/invalid values block the request and preserve the previous result.
  • Optional blank values remove their optional SQL blocks.
  • Enter/blur commit immediately; text typing debounces.
  • Refresh runs with the current shared persisted values.
  • Edits update and persist the same value/activation stores used by SQL Browser and dashboards.
  • A successful detached run records recent bound values through the shared recorder.
  • Failed, aborted, validation-blocked, or stale runs do not record recents.
  • Detached edits do not automatically run or replace results in the other surfaces.

Requests

  • Opening Expand performs no extra query.
  • A filter commit runs exactly one detached request.
  • A newer run wins over an older response.
  • Closing aborts an in-flight request.
  • Detached running state does not modify global state.running.
  • Main result/history/sort/view remain unchanged.
  • Current detached result is used by Copy.
  • Row cap and capped indicator are preserved across reruns.
  • Originating ClickHouse session is used when present.
  • Shared varValues and filterActive are persisted on commit.
  • Successful winning responses record their immutable boundParams.
  • Errors, aborts, stale responses, inactive optional parameters, and disabled-history mode do not record recents.

Detached-document behavior

Run the same cases for:

  • real child-tab document;
  • popup-blocked overlay fallback.

Verify that all nodes are created in the correct document realm and that cell drawers, controls, panel renderers, and event listeners are cleaned up.

Acceptance criteria

  • Expanded Data opens immediately with the existing result and issues no extra request.
  • The detached header shows the captured query title instead of the generic Data label.
  • A non-empty captured description appears below the title with two-line clamping; an empty description consumes no space.
  • The browser tab title matches the captured query title.
  • It exposes a local Table | JSON | Panel switcher.
  • Parameterized source SQL produces a full filter row using the shared field controls.
  • Filter commits and Refresh re-run only the detached query.
  • Detached filter values and optional activation are local and do not mutate global workbench/dashboard values.
  • The exact expand-time source SQL and originating session are used even after editor/tab changes.
  • Missing or invalid values block only the detached refresh and leave the previous result visible.
  • Concurrent/stale requests cannot overwrite newer detached results.
  • The main result, history, view, sort, panel config, and global running state are untouched.
  • Copy uses the latest detached result.
  • Child-tab and overlay-fallback paths behave identically.
  • Shared filter-field logic is not duplicated across dashboard/workbench/detached surfaces.
  • Per-file coverage gates hold; no new runtime dependency.

Non-goals

  • Editing panel configuration in the detached view.
  • Live synchronization with later editor text, runs, or panel edits.
  • Automatically re-running SQL Browser or dashboard queries when a shared value changes in the detached view.
  • Multi-statement/script execution.
  • Raw FORMAT or EXPLAIN detached reruns.
  • Exporting an uncapped result to disk.
  • Dashboard query-backed Filter-source option-query behavior from Dashboard: multi-filter option bundles, shared preview, and role-aware result selector #160.
  • Keeping an already-open detached view functional after its opener application is closed or reloaded.

Dependencies and sequencing

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions