Skip to content

FE-614 - Quick filter every search attribute value in the configurable tables - #3939

Open
rossedfort wants to merge 16 commits into
mainfrom
rossedfort/fe-614-all-search-attribute-values-should-have-a-quick-filter
Open

rossedfort wants to merge 16 commits into
mainfrom
rossedfort/fe-614-all-search-attribute-values-should-have-a-quick-filter

Conversation

@rossedfort

@rossedfort rossedfort commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Description & motivation 💭

Only Keyword and Text cells offered a quick filter button, so a column like
StartTime, HistoryLength or TemporalChangeVersion could be read in the table
but not filtered from it. Every search attribute value in the configurable tables
can now seed a filter from the cell it is displayed in.

Clicking a cell's filter button opens a popover anchored to that cell, showing the
operators valid for the attribute's type with the cell's own value prefilled.
Nothing is applied until Apply (or Enter). The exception is a status column, which
still applies = in a single click: a status filter renders as the multi-select
chip rather than the operator form, and isTextFilter refuses status filters
outright, so the editor would otherwise fall through to a generic equals select.
That is the whole rule — requiresOperatorChoice asks whether the attribute is a
status, not what its default operator happens to be.

The popover reuses the filter bar's own form rather than reimplementing it. The
per-type operator toggles, date and time pickers, chip input and timezone handling
lived inside dropdown-filter-chip.svelte; they move to filter-editor.svelte,
which the chip and the cell popover both render. Input ids are namespaced by the
caller, since a chip and a cell popover can now be open at the same time.

The rest is removing near-duplicate implementations. Workflows, activities and
Nexus operations each had their own filterable-table-cell.svelte and each table's
body cell repeated the same derivation — label to attribute, attribute to type, row
to value, value to filter. Those collapse into:

  • $lib/utilities/query/quick-filter.ts — the pure logic. Choosing the operator a
    popover opens on, coercing a cell value into something the query grammar accepts,
    and toggling a filter. Values arrive in whatever shape the API returned, so a
    datetime may be an ISO string, an epoch number, a Date or a protobuf
    Timestamp; normalizing is per-type rather than a String() call. A value that
    cannot be filtered on returns null, which is how a cell decides to render no
    button.
  • quick-filter-table.ts and quick-filter-cell.svelte — one resolver and one
    cell. A table supplies a column config and a value getter and needs nothing else.
  • A column-search-attributes.ts per table, mapping a column label to its search
    attribute and to the value to filter by.

ExecutionDuration is filterable, which needs care: it is typed Int but carries a
duration, and the displayed text is not reusable as a value. formatDistanceAbbreviated
(workflows, Nexus) delimits units with a space, while formatDurationAbbreviated
(activities) falls back to ', ' — stripping spaces from the latter yields 5m,10s,
which is not a duration. The value is built from the row's own data instead, truncating
rather than rounding so a >= filter on a row's duration still matches that row.

Filters are built on click rather than during render, since constructing one eagerly
per cell would mint a filter id for every row that is never used. Applying replaces
any existing filter on the same attribute rather than appending, so clicking down a
column moves the filter instead of accumulating contradictory clauses, and a
multi-status OR group collapses to the status that was clicked.

The buttons themselves no longer cost layout. Every filterable and copyable cell
previously reserved 64px of right padding for buttons that only appear on hover —
328px of a 1598px workflows table, about a fifth of it, for something invisible until
you hover. They are absolutely positioned over the right edge of their own cell, so
no column reserves width and it stays obvious which value they act on, at the cost of
covering the tail of a long value.

Screenshots (if applicable) 📸

Design Considerations 🎨

The operator a popover opens on is still type-derived — >= for datetimes, in for
keyword lists, >= for measures like HistoryLength, HistorySizeBytes,
StateTransitionCount and ExecutionDuration, = otherwise. Since the user now
confirms before anything is applied, that is a starting point rather than a guess the
table makes on their behalf.

Two things worth a look in review:

  • A Datetime popover opens in absolute mode and keeps the cell's exact timestamp
    unless the pickers are touched. The pickers only carry seconds, so recomputing from
    them would round …01.726484047Z to …01.000Z.
  • The workers table has the quick filter but not the popover. Its cell keeps a
    separate implementation for the SDK column, which applies SdkName and SdkVersion
    as one click. Its StartTime column is therefore the one place a Datetime still
    filters in one click, with >=.

Testing 🧪

How was this tested 👻

  • Manual testing
  • E2E tests added
  • Unit tests added

Unit tests cover quick-filter.ts (operator choice, value coercion per attribute
type, toggle and active state, and a round trip from filter to query string and back
for every type), the duration conversion, quick-filter-table.ts, and the workflows
column mapping. Integration tests cover the popover end to end in
workflows-search-attribute-filter.desktop.spec.ts: a column asking for an operator
and applying nothing until confirmed, applying with a chosen operator, Status
filtering in one click, Enter applying, and that no column reserves width for the
buttons.

Steps for others to test: 🚶🏽‍♂️🚶🏽‍♀️

  1. Open the workflows list on a namespace with a few hundred executions.
  2. Add columns of varied types — Start, History Length, Execution Duration,
    Change Version, a custom bool or int search attribute.
  3. Hover a cell and click the filter button. A popover should open against that cell
    with the value prefilled and an operator preselected, and nothing should be
    filtered yet.
  4. Apply with the button and with Enter. Both should add one clause.
  5. Change the operator before applying — a Start cell with Before should yield
    <= on the raw timestamp.
  6. Hover a Status cell and click. It should filter in one click with no popover,
    and clicking again should clear it.
  7. Click the same column on a different row — the clause should move rather than a
    second one being added.
  8. Confirm Parent Namespace shows no filter button, and that no column is padded
    for the buttons when nothing is hovered.
  9. Repeat on the activities, Nexus operations and workers tables.

Checklists

Draft Checklist

  • Manual pass over all four tables
  • Screenshots
  • Check the popover against the first and last rows, where it flips to stay on screen

Merge Checklist

Issue(s) closed

Closes FE-614

Docs

Any docs updates needed?

None.

@vercel

vercel Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
holocene Ready Ready Preview Oct 7, 2026 7:18pm UTC

Request Review

@rossedfort
rossedfort force-pushed the rossedfort/fe-614-all-search-attribute-values-should-have-a-quick-filter branch from 692e1d1 to 89a24d9 Compare September 17, 2026 17:50
Base automatically changed from rossedfort/fe-734-add-resizable-columns-to-the-workflows-table to main September 17, 2026 19:48
Move getDefaultConditional out of search-attribute-menu.svelte and add the
value formatting, toggle and active-state helpers that table cells need to
build a filter for any search attribute type (FE-614).
Filterability is now derived from the column's search attribute and its type
rather than a hardcoded label list, so Datetime, Bool, Int, Double and
KeywordList values get a filter button with an operator that suits the type.
Archival keeps the columns it supported before (FE-614).
The workflows, activities and nexus tables each carried a near-identical
filterable cell, so the workflows one becomes a shared component the others can
use. Datetime values now normalize a protobuf Timestamp, Date or epoch number
to an ISO string, since a table cell can hold any of those and only the string
form belongs in a query (FE-614).
…us and workers

Activities, nexus operations and workers now derive filterability from the
column's search attribute and type against their own attribute store, so
Status, timestamps and counts get a filter button alongside the ids they
already had. Worker start time filters on the raw timestamp rather than the
string it renders (FE-614).
@rossedfort
rossedfort force-pushed the rossedfort/fe-614-all-search-attribute-values-should-have-a-quick-filter branch from 89a24d9 to 1e7725f Compare September 29, 2026 16:52
Painting the flex container that holds them meant the two round button
backdrops blended into one flat rectangle. The buttons already carry their own
backdrop, and dense cells truncate their values, so the container does not need
one (FE-614).
… type

Asserts the query each cell emits: a Status cell round trips and toggles back
off, a Start cell uses >= with the raw timestamp rather than the text the cell
renders, a multi-status group collapses to the status that was clicked, a Bool
goes in unquoted, an Int of zero is still a value, and a column with nothing in
it offers no button (FE-614).
@rossedfort
rossedfort marked this pull request as ready for review September 29, 2026 17:49
@rossedfort
rossedfort requested a review from a team as a code owner September 29, 2026 17:49
…ation

HistoryLength, HistorySizeBytes, StateTransitionCount and ExecutionDuration now
read as "at least this much" rather than an exact match, which is almost never
what is wanted of a measure. ExecutionDuration becomes filterable: it is typed
Int but carries a duration, built from each row's own data because the display
formatters delimit their units and one of them uses a comma (FE-614).
The chip's per-type operator toggles and value inputs are exactly what a
cell-anchored operator popup needs, so they move into a component both can use.
Input ids are now namespaced by the caller, since two editors can be open at
once; the chip keeps its own prefix so its selectors are unchanged (FE-614).
A Datetime, KeywordList or measure column now opens a popup on the cell with
the operators valid for its type and its own value prefilled, instead of the
table guessing. Keyword, Text and Bool still filter in one click.

The popup is anchored to the filter button, which only exists while the cell is
hovered, so the cell keeps it mounted while the popup is open. Dismissal checks
containment rather than propagation, since Svelte delegates events at the root
and the menu's stopPropagation runs on the same node as the listener. The
editor keeps a seeded timestamp as given unless the pickers are touched, which
would otherwise round it to the second (FE-614).
Each table repeated the same derivation — label to attribute, attribute to
type, row to value, value to filter — and its own copy of a label lookup that
was identical in all three. That moves into a pure resolver plus one cell that
takes a column config, so a new table needs a config and a value getter and
nothing else. The resolver gains direct tests it only had through components
before (FE-614).
max-w-fit without min-w-0 resolved to the date picker's max-content width, so
the popup opened at roughly 740px against the filter chip's 390px (FE-614).
Both conflicts came from FE-613's not in operator (#3961) landing in code this
branch had moved. Its listConditionalOptions change applies to filter-editor,
which now owns that form, and its KeywordList spec selects the list input by an
id the extraction namespaced so two editors can be open at once.
Every filterable and copyable cell reserved 64px of right padding for buttons
that only appear on hover, costing 328px of the 1598px workflows table. The
buttons now portal into a small surface above the cell, so no column pads for
them and the value is never covered.

Leaving the cell for the buttons dispatches both events in one task and Svelte
batches the updates, so they survive the reach without a hide delay. A delay
would trail popovers behind the pointer across a row (FE-614).
The popup appeared only where the default operator was not `=`, which left
Keyword, Text and Bool columns applying a guess in one click. It now appears
everywhere except a status column, which keeps its single click because a
status filter renders as a multi-select chip rather than the operator form -
isTextFilter refuses status filters outright, so the editor would fall through
to a generic equals select.

The per-type conditional stays, but now only seeds the operator the popup opens
with rather than deciding whether one opens (FE-614).
The buttons floated above the cell, which left it ambiguous which row and value
they would act on. They sit over the right edge of their own cell again, which
can cover the tail of a long value and is the clearer trade. They stay absolute,
so no column reserves width for them, and being back inside the cell removes
the portal and the hover bridging it needed.

The popover opens without taking focus, so Enter could not reach the form on its
own. It now applies from anywhere in the popover, leaving Enter alone where it
already means something: a toggle, Apply itself, or committing a keyword chip
(FE-614).
#3987 added custom search attribute rendering and filtering to the activities
and Nexus tables using the per-column structure this branch replaced, so its
intent is ported rather than its diff. Its SearchAttributeValue component now
renders the per-type display all three cells were duplicating, the two value
getters fall through to a row's indexed fields, and activities take the decoded
ActivityExecutionListInfo.

Its isFilterableCustomSearchAttribute is deliberately not adopted: it narrows
custom attributes to Keyword and Text, which this branch supersedes by gating on
the search attribute store so every custom type can be filtered.

This branch was successfully deployed

1 active deployment
Preview — 67c9c620 Deployed Oct 7, 2026 by vercel[bot]
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