Skip to content

[draft] bt-ui: single page UI - #7127

Draft
gignat-dev wants to merge 45 commits into
mainfrom
dev/gignat/bigtrace_spa
Draft

[draft] bt-ui: single page UI#7127
gignat-dev wants to merge 45 commits into
mainfrom
dev/gignat/bigtrace_spa

Conversation

@gignat-dev

Copy link
Copy Markdown
Contributor

No description provided.

@gignat-dev
gignat-dev force-pushed the dev/gignat/bigtrace_spa branch 7 times, most recently from dce07e7 to b2a7ee0 Compare August 25, 2026 09:00
The History sidebar split runs across two sub-tabs (Ephemeral / Persistent),
so you could never see both at once and every Run had to point the sidebar
at the matching tab. Replace the tabs with a single list plus an
All / Ephemeral / Persistent segmented filter, defaulting to All.

- history_store: `filter` (all/ephemeral/persistent) replaces `activeTabKey`;
  a new pure `filterHistory()` does the narrowing. `setHistoryActiveTab` and
  its three call sites in the editor toolbar / query page are gone.
- query_history: fixed toolbar (filter + refresh) above a scrolling list.
- query_history_item: the kind now comes from the entry itself, and each row
  carries an Ephemeral / Persistent marker since the list mixes both.
- The flat segmented control the preset picker used moves to
  widgets/segmented.ts so both surfaces share it.
The right sidebar carried a second tab browsing the PerfettoSQL stdlib
(fetched from stdlib_docs.json). It is not part of querying at scale, and
it cost the History panel a tab strip. Delete it: History is now the whole
sidebar, rendered directly with no Tabs wrapper.

Removes sql_tables.ts, table_list.ts and sql_modules.ts (nothing else
referenced them, and the stdlib_docs.json fetch goes with them), and renames
the sidebar command to bigtrace.ToggleHistorySidebar now that there is only
one thing in there.
Every query started at 100 rows regardless of how it ran, and the trace
fan-out cap was whatever the backend's global default happened to be. Tie
both to the execution mode instead: ephemeral runs 1k rows over 10k traces
(a quick look), persistent runs 10k rows over 100k traces (a full sweep).

- query_tabs_state: MODE_DEFAULTS + helpers over the trace cap
  (explicit/effective/set/clamp). New tabs seed both caps from their mode;
  applyModeDefaults() moves them when the mode flips, but only where the
  value still holds the old mode's default — anything typed by hand, or set
  by a preset, survives. A run with no explicit cap ships the mode default
  rather than the backend's global one.
- editor_tab_view: the toolbar now shows Rows and Traces side by side, so
  what a run costs is visible without opening settings. The Traces control
  hides itself when the backend doesn't declare a trace cap.
- The backend's declared min/max for a numeric setting now survives onto the
  descriptor (it was folded into the zod schema and lost), so a default we
  seed ourselves can be clamped to what the deployment accepts.

The trace cap is a backend setting, so the UI has to name it: that coupling
is confined to TRACE_LIMIT_SETTING_ID and degrades to "no cap control" when
the backend doesn't advertise it.
Groundwork for the new-query launcher: presets become the unit of
configuration, so users need to save their own alongside the backend
catalog. A LocalPreset is just a TracePreset with an isLocal marker, so both
kinds render and apply through one code path; only the launcher's rename /
delete affordances branch on it.

- localPresetStore: one localStorage key, CRUD (save / overwrite by id /
  rename / remove / findByName for the overwrite prompt). Entries missing an
  id, name or SQL are dropped on read — the key is user-editable — and writes
  swallow quota errors rather than failing a save.
- presetFromTab() captures the EFFECTIVE settings, so a saved preset
  reproduces its run without depending on what else is registered.
- lastPresetIdState remembers what the last new query started from (the
  launcher preselects it); lastSetupState remembers the trace selection a
  query last ran with, to seed the launcher's custom form.

Store only — nothing renders it yet.
A new query used to open on an empty editor inheriting whatever the global
settings page happened to hold. Presets are becoming the unit of
configuration, so a tab now starts on a launcher: pick a preset (backend
catalog or one you saved), or configure the traces and options yourself.

- Tabs gain `configured`; while false the tab renders QueryLauncher instead
  of the editor. Tabs restored from storage or opened from History count as
  configured, so a reload never drops you back into the picker.
- The launcher preselects the preset the last query started from, marks it
  "Last used", and applies it in one click. Locally-saved presets carry
  rename / delete, and the custom path can save the current setup as one.
- The custom path embeds the existing settings form with the tab's bindings
  and adds a footer: back to presets / save as preset / start query.
- applyPresetToTab() replaces the tab-creation-only preset path, so a preset
  can also reconfigure an open tab. presetMatches() moves to
  query/preset_match.ts, shared by the form and the launcher.
- A preset no longer touches WHERE the traces come from: a catalog shared
  across users can't know that, and clearing it left the trace grid with no
  directory. Trace-source settings stick unless a preset names them, and a
  fresh tab inherits them from the last configured query (lastSetupState).
- The chip strip gains "Change setup" (back to the launcher, settings kept)
  and drops the trace-cap chip, which now has a toolbar control.
Three routed pages (home, query, settings) plus a left nav, for an app that
only ever does one thing: run queries. Presets moved into the tab launcher
and per-query settings into the tab itself, so nothing is left for the other
two pages to hold. Delete them.

- index.ts mounts QueryPage directly: no Gate, no route resolution, no hash
  router (router.ts / routes.ts / home_page.ts / layout/sidebar.ts and the
  cross-page queryState signals are gone), and no left-sidebar command.
- The topbar becomes the only chrome: brand, omnibox, and on the right the
  new connection control — the endpoint is a connection, not a query setting,
  so it's the one thing that stays global. It shows the backend host, opens a
  panel to edit it (with reload-to-apply and the config load error), and
  auto-opens when nothing is configured, replacing the old home-page
  "connect a backend" onboarding.
- The `auto` sidebar column in the shared ui_main grid collapses to zero with
  no sidebar element, so no shared style needed changing.
The settings page was built to work two ways: standalone (writing global
state) and embedded with per-tab bindings. The standalone half died with the
routes, and with it the idea of global query settings — a query is now
configured by a preset or by its own custom setup.

- settings_page.ts becomes query_settings_form.ts / QuerySettingsForm, with
  `bindings` required. Gone: the SettingsShell chrome and search box, the
  endpoint card (it lives in the topbar now), and the preset picker (the
  launcher owns choosing a preset). Every read/write goes straight through
  the bindings, so ~40 lines of "per-tab or global?" branching go too.
- The card styles ship with the SettingsShell widget, so the form imports
  that stylesheet directly rather than depending on the shell being mounted.
- trace_selection_state keeps only the trace grid's shown-columns preference
  — a display choice that isn't part of a query. The filter, processing order
  and result-metadata columns were global only to seed new tabs; they're now
  per-tab, restored from a tab's own snapshot or set by the launcher.
- Starting a query from the custom path remembers the preset it matches (a
  setup just saved as one), so the next tab preselects it.
The launcher commit carved out the trace-source settings: a preset that
didn't name them left whatever the tab already had, on the theory that a
shared catalog can't know where a user's traces live. That is the UI deciding
for the user. Drop the carve-out: applying a preset configures every
registered setting, and one the preset omits is turned off like any other. A
run that then has no trace source says so, and the user sets it.

Fresh tabs stop inheriting the last query's trace source for the same reason.
The launcher's custom path still seeds its form from the last setup — that's
a starting value the user sees and can change before starting the query, not
a decision made behind their back.
Nobody asked for it. The custom path pre-filled its form from whatever the
last query ran with, which is another decision made on the user's behalf and
one more piece of state to reason about. The form now opens on the backend
defaults, same as the settings it shows.

Removes lastSetupState / QuerySetup / setupFromTab entirely; the state module
keeps only the last-used preset id (which the user did ask for, to preselect
a preset on a new query) and is renamed to settings/last_preset_state.ts.
All / Ephemeral / Persistent encoded a two-value filter as a three-way mode,
where "All" was just both kinds at once — a third of a narrow sidebar row
spent on derivable state, in a control that still read like the tab strip it
replaced. Two independent checkboxes say what they do.

Unticking both is allowed and shows an empty list saying so, rather than
silently re-ticking one or quietly showing everything.

The store's filter becomes {ephemeral, persistent}; filterHistory() keeps an
entry when the box for its kind is ticked. The segmented control stays, still
used by the launcher's preset groups.
Applying a preset turns off every setting it doesn't name, and the trace cap
is a setting — so any preset from the reference catalog (which declares only
treat_trace_errors_as_warning) landed the cap in disabledSettings.
effectiveTabSettings drops disabled ids, so the run shipped no trace_limit at
all and fanned out over the whole corpus, while the toolbar read
"Traces: 100000". Typing a new cap didn't help either: setTraceLimit wrote to
querySettings, the input re-rendered with the typed value, and the disable
still won.

The cap isn't really a preset-scoped setting — it's a run control with a
per-mode default, like the row limit. So a preset that doesn't state one now
gets the default for its mode written explicitly, instead of being disabled.
A preset that does state one still wins.

Two supporting fixes so the controls can't lie again:
- setTraceLimit clears the disable, because the toolbar control IS the cap.
- The toolbar hides the Traces box, and the settings card reports no value,
  when the tab has the cap switched off — an uncapped run now looks uncapped.

Found by review; regression tests cover the omitted-cap, stated-cap and
re-enable paths.
Four things review turned up around the new surfaces:

- The connection button called itself connected whenever an endpoint was set,
  but the endpoint ships with a default — so a user who has never configured
  anything saw "cloud_done" and the corp host, and the auto-open onboarding
  never fired. Connected now means the backend answered
  (execConfigLoadError === undefined), the button says what it isn't
  connected to, and the panel opens itself once when there's no working
  backend.
- The launcher flashed "No presets yet" for the length of the /trace_presets
  round trip. It now shows a loading state, and when the catalog really is
  unreachable it says so and points at the connection button — the deleted
  home page had this guard and the port lost it.
- An uncategorised preset ("" is a valid category) never got its group
  preselected, because the launcher looked up the raw category while
  groupPresetsByCuj buckets it as "Other". The badge sat in a group the user
  had to go find.
- "Change setup" is disabled mid-run: it swaps the editor, status box and
  Cancel button for the launcher, where picking a preset would rewrite the
  tab under a query that's still executing.

Also retargets the empty-endpoint run error, which still sent people to a
Settings page that no longer exists.
Sweeping up after the revamp. ~230 lines of CSS that nothing renders any
more:

- .pf-bt-simple-table-list, orphaned with the stdlib schema browser;
- .pf-bt-preset-chip{,s} and the .pf-bt-preset-picker spacing rule, orphaned
  when the settings form lost its preset picker;
- the "Query" CTA rule in the Traces card header, orphaned with the button.

The endpoint, chip-modal and settings toggle/warning/hint/category-header
rules go too; those were already dead before this branch.

Also: comments naming the settings page, SettingsPage and traceOrderByState
now describe what's actually there, and four helpers that only their own file
uses (newLocalPresetId, modeDefaults, clampTraceLimit, explicitTraceLimit)
stop being exported.
There was no way to fork a query: you either edited a tab in place and lost
what produced its results, or saved the setup as a preset and applied it
somewhere else. "Duplicate" on the chip strip opens a copy in a new tab —
same SQL, settings, filters, metadata columns, order-by, mode and caps.

The copy carries no queryUuid, so it's unrun: running it creates its own
execution instead of adopting the original's, and reopening either from
History still lands on the right tab. Titles go "<name> copy", "copy 2", …
so repeated forks stay apart.
It sat in the wrapping chip row with Settings and Change setup, which read as
if it configured the run. It doesn't — it acts on the tab as a whole — so pin
it to the right edge, away from the configuration chips, where it also can't
be pushed around by a long filter row.
Renames the chip, QueryTabsState.cloneTab, and the title a clone gets
("<name> clone", "clone 2", …) so the button, the API and the tab title all
say the same word.
Missed when the file was added; presubmit's prettier check caught it.
The loading and error branches returned a bare EmptyState, skipping the
.pf-query-history wrapper and the caller's class. The panel lost its
height:100% flex column and sprawled — a tiny message adrift in a white
half-screen — and the toolbar went with it, so the refresh button that
retries the failed load wasn't there to click.

The frame is now unconditional: the kind filter and refresh always render,
and loading / error / empty all live inside the list area.
The CSP pinned style-src to two sha256 hashes. CodeMirror generates its
stylesheet at runtime, so no fixed hash can cover it: the browser refused it
and the query editor rendered unstyled — white background, gutter stacked
above the code, no syntax colouring — in any browser that enforces the
policy. The hashes have been there since Bigtrace UI V0 and have drifted
since.

Allow 'unsafe-inline' for styles, the same allowance ui/src/frontend makes
for the same reason.
"Change setup" swaps the editor for the launcher, and the only ways out were
picking a preset — which overwrites the tab's SQL and settings — or going
deeper into custom settings and pressing "Start query". Changing your mind
meant either losing work or guessing.

A back control now sits above the title, first in reading order: it restores
the editor and changes nothing. It appears only when the tab has something to
go back to (a query, a run, or results), so a brand-new tab isn't offered a
way back to an empty editor. That test reads the tab's own contents, so it
still holds after a reload.
It copied the executed SQL to the clipboard, from a toolbar sitting directly
below an editor showing that same SQL. Clone covers reusing a query properly,
and the row it lived on is better spent on the result controls.

The one thing it did that nothing else does is hand back the SQL a run
actually used after the editor has been typed over — but that's the "what did
this run with?" question, which wants a snapshot of the whole submitted
configuration, not a copy button for one field of it.
The settings form rendered two ways: inline in the launcher, and inside a
modal opened from the chip strip. Same content, two containers, and two chips
("Settings", "Change setup") that differed only in which half of that content
they landed on.

Now there is one surface — the launcher, in the tab — and one button. Settings
opens it on the settings form; the preset gallery is a click away inside it,
where "Back to presets" already lived. Which half it shows is state on the
tab, so both entry points and the surface's own navigation share it.

Falling out of that:
- The modal opener is gone.
- The primary action reads "Done" for a tab that already has a query, and
  "Start query" only when there isn't one to go back to.
- "Back to presets" hides mid-run, because applying a preset would rewrite a
  tab whose query is still executing. Editing settings for the next run stays
  available, which the modal used to allow and the old chip did not.
Presets belong to starting a query; settings belong to one that exists. From
a tab that already has a query, clicking a preset isn't configuring it — it
replaces the SQL, title, settings, filters, columns and caps wholesale, with
no undo, one click inside a button labelled Settings. Opening a new tab, or
Clone, does that job without costing the work in front of you.

So the surface now offers exactly one way back, and which one depends on
whether the tab has something to go back to: a new tab gets "Back to
presets", an existing query gets "Back to query". The overwrite hazard is
simply unreachable from a working tab.
The settings form writes every edit straight onto the tab, so the two ways out
of it — "Back to query" and "Done" — did the same thing. Placed side by side
they read as cancel and commit, and someone who tried a filter and pressed Back
to undo it found the filter kept.

Back now means what it says. Opening Settings on a tab that has a query keeps a
snapshot of the run configuration (settings, on/off state, trace filters,
metadata columns, order, caps, mode); Back restores it, Done drops it. The
snapshot lives on the tab as view state — the tab stays configured while the
form is up, so a reload closes Settings with the edits kept, as Done would.
A new tab configuring its first query has no such session: nothing to go
back to.
The preset card renders a description, but a preset saved from the UI never
had one: the save dialog asked for a name and a group, and the only edit
afterwards was a rename. So a user's own presets sat in the gallery as bare
titles next to the catalog's described ones.

The save dialog gains a Description field, and the card's rename becomes an
edit of name, group and description together — one form behind both. The
name is checked as it's typed: a name the catalog already uses is refused
outright, since a local twin couldn't be told apart in the gallery; a name the
user already has still offers to overwrite when saving, and is refused when
editing another preset into it. A preset keeps its own name while being
edited, even one that already shadows the catalog.
The launcher's primary button doubles as "Start query" on a new tab and
"Done" on one that already has a query, and both ran the same handler: work
out whether the tab's setup matches a preset and record that (or nothing) as
the preset to preselect next time. That is right for starting a query by hand.
For Done it meant that opening Settings on any tab and changing an option
cleared the "last used" badge for the next new tab, though nothing had been
started.

Only Start query records it now.
"Back to query" was named when it was pure navigation; now that it puts the
configuration back to what it was, the label undersold it and left the
discarding to a tooltip. "Done" beside it read as its opposite without saying
so either.

Cancel and Apply say what happens. Apply rather than Save: "Save as preset…"
sits in the same footer with a Save button in its dialog, and nothing here is
saved anywhere a user would look — the settings simply become the query's,
which is what Apply means elsewhere in the UI (DataGrid filter menu, Data
Explorer nodes). The new-tab footer is unchanged: "Back to presets" is a step
back that keeps what was typed, and "Start query" is not an edit.
The settings form chose its footer by whether the tab had SQL, a run or
results. A configured tab with none of those — Start query pressed on an
empty form — then got the new-tab footer from the Settings chip: "Back to
presets" and "Start query" instead of Cancel and Apply. Worse, Back to presets
led to the gallery with the Settings session still open, and picking a preset
there applied it without ending the session, so the editor never came back.

The footer now follows the session: opened from Settings on a configured tab
it is Cancel / Apply, whatever the tab holds; only a new tab's form offers
Back to presets. Applying a preset ends any session and leaves the launcher
outright, so no path through the gallery can strand a tab there.
Presets left the Settings surface when applying one meant replacing the tab
wholesale. But a preset need not carry a query at all — a setup-only preset
is a trace source, a selection, options, caps and a mode — and "run my query
with that setup" is a real thing to want from a query that already exists.

So the settings form gets a picker at the top, "Settings from preset". From
there a preset lends its setup only: settings (what it doesn't name is off, as
ever), trace selection, result columns, order, caps and mode. Never the SQL,
never the title — Settings is where you decide how a query runs, not what it
runs; starting a query from a preset, SQL and all, stays in the launcher
gallery. Like every edit in the form it is provisional: Cancel undoes it,
Apply keeps it.

The picker doubles as an indicator. It reads the preset whose setup the tab
runs with right now, strictly — named settings at their values, everything
else off, the trace cap aside — and "Custom" once nothing fits. Query-only
catalog presets share one empty setup, so the one last picked is read back
for as long as the setup still is that setup.

Since a preset's setup includes the row cap and the mode, the form now shows
them too (a "Run" section: Persistent, Row limit), so nothing the picker
touches is off-screen. Gallery cards without a query say "Setup only", and
applyPresetToTab is split so the gallery applies setup + query while the form
applies setup alone. Two binding members from the modal era that nothing
called (getSql, setQueryAndTitle) go; the run controls take their place.
The catalog's query-only presets all share one empty setup, so a tab started
from "OOM kills" read back in the settings form as "Settings from preset:
LMK events" — the first twin. The picker preferred the preset picked in the
form, but only for the life of that form; a preset applied from the gallery,
or anything after a reload, had no such memory.

The tab now records the preset last applied to it, from either place, and
persists it with the rest of the tab (clones inherit it). The picker prefers
that one for as long as the setup still is that setup.
The "Settings from preset" dropdown was the wrong shape: a select is a
value, not a set of things to look at, and it hid the cards — descriptions,
groups, the setup-only tag, the edit and delete tools — behind names.

The settings form now carries the launcher's own gallery, folded under a
"Presets" heading in the section-title style; open it and the cards are
there, grouped as in the launcher. What picking one does is unchanged from
the dropdown: the preset lends its setup — settings, trace selection, result
columns, order, caps and mode — and the query stays the user's, provisional
until Apply. The card whose setup the tab runs with is marked "Current", and
the folded heading says the same ("Setup from X", or "Custom setup"), so the
answer is there without opening it.

To share the cards between the two places, the gallery becomes its own
component (PresetGallery: grouping, cards, local edit / delete) and the
save / edit / delete dialogs move to preset_dialogs.ts; the launcher renders
the gallery for starting a query and passes the folded one to the form as a
header slot. isLocalPreset lives with the local store now.
"Pick a preset to use its settings here. The query is not changed." —
the list of what a setup contains belongs in the docs, not in the form.
Settings and Clone were chips — display elements with no hover state and, in
Clone's case, not even a pointer cursor — sitting among chips that really
are display. They are the two actions on that row, so they are buttons now:
compact, filled, with the hover and press states that come with that.

Cancel in the settings footer was a bare text button next to a filled
Apply, easy to miss for the one control that undoes everything. It is filled
like a dialog's Cancel, and so is Back to presets on a new tab, so the way
out is as visible as the way forward.
The folded Presets section was rendered for both ways into the settings
form. On a new tab that made no sense: the gallery is one step back behind
"Back to presets", and there a preset starts the query, SQL and all — while
the same cards folded into the form would have lent their setup only. Two
galleries, two meanings, one screen.

The fold appears only when Settings was opened on a query that exists,
which is the case it was made for.
Starting a query was two screens — the preset gallery, and the settings form
behind "Configure custom settings" with "Back to presets" as the way back —
while Settings on an existing query was a third arrangement of the same
parts. Now there is one page with two sections, Presets and the settings
form, and only which section is folded depends on the tab.

A tab with no query yet: title, then Presets open (a card starts the query
outright, whole preset), then "Custom settings" folded with the full form
inside; the footer offers Save as preset… and Start query. Settings on a
query that exists: Presets folded (a card lends its setup only), the form
open, Cancel and Apply in the footer. Same page, same parts, no modes:
setupMode, the two navigation buttons and the switching code are gone.

The settings form takes a `fold` option for the folded case, and the
launcher's styles collapse to the one layout — a scrolling body over a
footer.
On a new tab a preset card applied the whole preset and opened the editor in
one go; in Settings on an existing query a card lent its setup and waited
for Apply. Two behaviours for the same cards on what is now the same page.

Now a card only ever fills the page in. On a new tab that means the whole
preset — query, title, setup — with the card marked "Selected" and the
folded "Custom settings" heading reading "From X", so the setup can be opened
and adjusted (a catalog preset's missing trace source, say) before Start
query opens the editor. In Settings a card still lends its setup only, and
Apply keeps it. One rule: cards fill, buttons commit.

Reading the selection back is strict — the query and the setup must be
exactly the preset's — so a fresh tab is nobody's and the last-used
suggestion shows until a card is picked. Start query records the selected
preset as last used. applyPresetToTab no longer configures the tab (that is
Start query's job), and the unused addTabFromPreset goes.
It existed for one stale state — a tab persisted mid-Settings by an older
build, with a query but no configuration flag — and keyed on "the tab has
SQL". Now that a card fills a new tab's query in without leaving the
launcher, that key fired on the ordinary path and offered a "Back to query"
above the presets of a tab that had never had one. Nothing produces that
stale state any more; the link and its predicate go.
A tab took the name of the preset it started from, the first line of its
SQL on the first run, or "<name> clone" — three schemes, none asked for.
Tabs now keep the default "Query N" like the main UI's query page, and
change only when the user renames one; restored tabs keep whatever they were
called. A clone is a new tab and is named like one.

The SQL-derived title had a second job — telling an empty editor from one
with something to run — which hasQueryText keeps.
One form carried everything a query runs with, so the launcher page — and a
preset, and the Settings button — dealt in all of it at once. The feedback:
a query's identity is its traces and its SQL; the rest is knobs.

So the configuration splits by what it's about. The launcher page (a new
tab, and the chip-strip button on an existing query, now labelled "Trace
Selection") holds presets and trace selection only, flat — no accordions:
the Presets section, then the trace grid leading the source settings and the
result-metadata columns. The remaining settings — the trace cap and every
non-trace category — move to a Settings modal behind a gear at the end of
the run toolbar, with the same contract as the page: edits land on the tab
as they're made, Apply keeps them, Cancel (or closing the dialog) restores
the configuration from when it opened. The Traces box leaves the toolbar —
the cap is edited in the modal — keeping the default surface to Persistent,
Rows and the gear.

The chip strip follows the same line: it summarises which traces the query
runs over, so it chips only trace-selection settings (plus the grid filters,
as before) — options aren't chipped, they live behind the gear.

Presets shrink to match: a preset is a trace selection and a query — source
settings, grid filter and order, result columns, SQL — and nothing else.
Applying one no longer touches options, caps or mode (what a stale catalog
entry still names outside that scope is ignored), the old "write a
mode-default trace cap" special case disappears, saving a preset captures
only its scope, and matching compares only its scope. Within that scope a
preset is still the whole selection: trace settings it doesn't name are
turned off.
The card gallery was a lot of chrome for a one-line choice. In its place,
one row: the category selector as before, and beside it a dropdown holding
that category's presets, with edit and delete alongside when the named
preset is the user's own. Descriptions ride as tooltips — on each option
while choosing, on the control once something is chosen — not as page text.

Clicking a category applies one of its presets outright — the one last
applied there while the page is up, else the tab's last preset when it
belongs to the category, else the first (deterministic, never random) — and
the dropdown refines the choice within it. So once the picker has been
touched there is always a preset applied, and its value always names the
one the page is actually filled from. Until then it reads "— choose a
preset —", opens on the last-used preset's category and marks that preset
"· last used" inside the list — a suggestion, not a choice; "· setup only"
is marked the same way.

Picking stays provisional as everywhere: the whole preset on a new tab, the
trace selection on an existing query, the footer commits and Cancel
restores. The PresetGallery component and the card styles go.
Two column pickers shared the page: "Shown columns" (a global display
preference for the trace grid) and "Query Result Columns" (the per-tab
trace_metadata_columns attached to result rows). Near-identical names, one
distinction nobody should have to hold. Now there is one: the grid's shown
columns ARE the metadata attached to every result row. The picker is backed
by the tab's traceMetadataColumns — so presets capture it, applying a preset
sets the grid view, Cancel/Apply covers it, and the snapshot reports it —
and the global display preference (traceColumnsState) is deleted. Emptying
the set falls back to the backend's defaults: the grid needs a column, and
"attach nothing" isn't a state the page offers.

The page also stops narrating itself. The "Presets" and "Trace selection"
section headings go, replaced by one heading for the whole activity —
"Select a trace corpus", with "Start from a preset for your vertical, then
filter the table down to the traces of interest" beneath. The grid card
drops its "Traces" title and its filter-or-sort caption, and the refresh
button moves into the grid toolbar, left of the trace count.
A backend may declare a trace_uuids setting: a second, mutually exclusive
way to select the corpus — instead of a source plus the grid filter, an
explicit list of traces. Showing it as an ordinary settings card would miss
the point; when the list is the selection, nothing else on the page applies.

So it renders as a mode, not a card. When the config declares the setting, a
"Paste trace UUIDs…" entry joins the corpus page's picker row; taking it
swaps the grid and the source cards for a single paste box — commas, spaces
or newlines, deduped, with a live count — and "Back to filtering" swaps them
back. The chip strip summarises it as "Trace UUIDs: N" and shows none of the
filter-mode chips the list overrides.

The mode is derived, not stored: the tab is in it exactly when it holds its
own enabled trace_uuids entry. Requiring the explicit entry keeps a fresh
tab out of the mode even though the config (which declares the setting)
arrives only after the first tab is created — the tab-creation mirror of
globally-disabled settings can't cover settings it hasn't seen yet. Because
it's ordinary settings plumbing beyond that, everything follows for free:
clones carry the list, Cancel restores it, Apply keeps it, the submit-time
snapshot reports it, saving a preset captures it, and applying a preset that
doesn't name it turns the mode off. Toggling is non-destructive both ways —
the filter-mode configuration is hidden while the mode is on, and the list
survives leaving and re-entering it.

Wire contract (documented): enabled + non-empty trace_uuids means the
backend selects exactly those traces and ignores the source settings, the
trace_filters / trace_order_by fields and trace_metadata_columns; an empty
list reads as absent. Backends declare it stringArray, disabled by default.
The backend promoted trace_limit: it travels as a top-level field on
/execute_* now, like the row limit, and no longer appears in
/bigtrace_execution_config. That deletes a whole apparatus the UI had built
around it being a setting: the derived per-mode value injected into the
settings payload, the clamp against declared min/max, the enable/disable
state meaning "uncapped", and the fresh-tab replacement of the backend's
default.

In its place, a plain number on the tab — tab.traceLimit — handled exactly
like tab.limit: defaulted per execution mode (10k ephemeral / 100k
persistent), moved with the mode while untouched, persisted with the tab,
copied by Clone, covered by the settings snapshot so the gear modal's
Cancel/Apply polices it, shipped as trace_limit on every run, and restored
from the traceLimit snapshot field when a query is reopened from History.
The gear modal's Trace Limit card becomes a plain number input (no toggle,
no reset-to-declared-default), and the gear is always present — the cap is a
universal request field. The trace_limit setting id disappears from the UI
entirely: with one backend, already migrated, there is nothing to be
defensive about.
Corpus-page picker: the native select becomes a PopupMenu — an outlined
trigger button (steady width, name + arrow) opening app-rendered rows
with a check on the applied preset and descriptions as tooltips. A muted
middot separates the category strip from the trigger, and the "Paste
trace UUIDs…" entry anchors to the row's right edge with an auto margin,
so it holds even when the row wraps on narrow widths.

Preset management moves out of the picker into a Manage presets… button
in the launcher footer beside Save as preset… — reachable in either
selection mode (the dropdown was unreachable exactly in UUID mode, where
the picker hides). The modal edits and deletes in place: a row expands
into a contained editor (name/group/description, the save dialog's
conflict validation) or swaps its actions for a Delete/Cancel confirm —
no dialog ever replaces the modal. The editLocalPreset and
deleteLocalPreset dialogs and the applied-preset-only inline buttons go
away; managing a preset no longer requires applying it first.

Toolbar and labels: Format moves next to the Run cluster (divider added
there, the orphaned one before Persistent removed); the gear modal is
titled Advanced Query Settings; the corpus grid's column picker is named
Query result columns; the "setup only" marker is dropped from preset
labels and the manage list.
The backend can now fan a query out over one arm of an experiment. This
puts that in the user's hands and carries it everywhere the rest of the
trace selection already goes.

The trace grid's header gains an Experiment Filter. Searching asks
/v1/experiments (one capped page, debounced, never for an empty box —
matching the whole catalog is a round-trip nobody can use), and each
result offers both arms: id first, then the name, which routinely runs
past a hundred characters and so is clamped everywhere with the whole of
it in the tooltip. An arm the caller can't query is prefixed "(Not
available in Telemetry datasets)" and stays selectable — the backend
denies at execution, not the UI. Picking one replaces the button with the
arm toggle, the chosen arm, and a way to clear it; switching arm or
clearing refetches the grid, so what it lists stays what a run would
process.

From there it travels as any other part of the selection: shipped as the
top-level experiment_filter on /execute_* and /trace_metadata, persisted
with the tab, copied by Clone, restored from the submit-time snapshot
when a query is reopened from History, captured by and applied from a
preset (which a preset states as ids and arm alone), compared when the
launcher works out which preset a tab is running, held provisional by
Trace Selection's Cancel/Apply, and summarised in the chip strip. Ids are
numbers on the wire in both directions and stay numbers throughout, so
there is no conversion to keep in step; display names never travel — they
are resolved from /v1/experiment_metadata once per id, and only onto the
filter that asked for them.
@gignat-dev
gignat-dev force-pushed the dev/gignat/bigtrace_spa branch from b2a7ee0 to 6ec15fd Compare August 25, 2026 09:42
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.

1 participant