feat: per-row filters saved with a board - #4470
brantsrasmus wants to merge 4 commits into
Conversation
Boards could only be filtered through the URL, so the filter was lost on save and every row necessarily shared one filter set. That makes the "one row per segment" layout — e.g. a row of metrics per region — impossible to express. Closes umami-software#4468. Row filters are stored on the row in `board.parameters`, which needs no migration: the column is JSON and the update schema passes parameters through untouched. - `BoardRow.filters` holds the filter set structured, so the dialog can reopen with the saved values. Event property filters are left out: the dialog only offers them on /events, which no board component renders. - `FilterScopeProvider` supplies extra filter params to everything below it, and `useFilterParameters` merges them in, so every existing query hook picks row filters up with no change of its own. - `mergeFilterParams` renumbers colliding params (`path` -> `path1`, `spf0` -> `spf1`) so a row filter narrows the board's filter instead of silently replacing it. - Scoped filters apply even when a share link sets `allowFilter: false`, since they are part of the board definition rather than the viewer's selection. - Board edit gains a per-row filter button reusing `FilterEditForm` (now seedable via `defaultValues`); both edit and view show read-only chips for a filtered row. Rows without filters render exactly as before.
Adds a data-test hook to the row filter chips and a Playwright spec asserting the two things that were broken: a row filter survives the board save/reload round trip, and it scopes only its own row's queries (spf param present for the filtered row, absent for an unfiltered one).
|
@brantsrasmus is attempting to deploy a commit to the Umami Software Team on Vercel. A member of the Team first needs to authorize it. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Greptile SummaryThis PR stores structured filters on individual board rows and injects them into descendant analytics queries through a new filter-scope context.
Confidence Score: 4/5The mixed-entity row failure should be fixed before merging because saved website-specific filters can make sibling columns report incorrect analytics. One row-wide scope is consumed by every descendant query even though each mixed-board column may target a different entity, while filter definitions are resolved against only the first website. Files Needing Attention: src/app/(main)/boards/[boardId]/BoardViewRow.tsx, src/app/(main)/boards/[boardId]/BoardEditRow.tsx, and src/app/(main)/boards/[boardId]/useBoardRowWebsiteId.ts
|
| Filename | Overview |
|---|---|
| src/app/(main)/boards/[boardId]/BoardViewRow.tsx | Applies saved row filters to all columns, which breaks mixed-entity rows when filters are website-specific. |
| src/app/(main)/boards/[boardId]/BoardEditRow.tsx | Adds row-filter controls and scopes design-mode components, inheriting the same mixed-entity behavior. |
| src/app/(main)/boards/[boardId]/useBoardRowWebsiteId.ts | Selects only the first website in a row for filter editing and labels despite rows supporting independently bound entities. |
| src/components/hooks/useFilterParameters.ts | Merges contextual row filters with URL filters and preserves scoped definition filters when viewer filtering is disabled. |
| src/lib/params.ts | Adds consistent serialization and collision renumbering for row-filter parameters. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Saved[BoardRow.filters] --> Serialize[boardRowFiltersToParams]
Serialize --> Scope[FilterScopeProvider]
Scope --> Params[useFilterParameters]
Params --> A[Column A query: Website A]
Params --> B[Column B query: Website B / Pixel / Link]
Reviews (1): Last reviewed commit: "test: cover board row filters end to end" | Re-trigger Greptile
| } | ||
|
|
||
| return ( | ||
| <FilterScopeProvider params={scopeParams}> |
There was a problem hiding this comment.
Row scope crosses entity boundaries
When a mixed-board row contains columns bound to different entities, this provider sends the same website-specific segment, cohort, or property filters through every column's query. Filters resolved against the first website are therefore applied to other websites, pixels, or links, causing those columns to report empty or incorrect analytics.
Knowledge Base Used: Boards, Pixels, and Links
There was a problem hiding this comment.
Good catch — fixed in 7a0a6f8.
You're right that a mixed board resolves entities per column, and pixels and links have no session properties or segments at all, so a row-wide scope was wrong.
Row filters now record the website they were authored against (BoardRowFilters.websiteId), and useBoardRowScope applies the scope per column: only columns resolving to that website get the FilterScopeProvider, everything else renders unfiltered exactly as before. Rows that mix entities also show the website name beside the filter chips so it's clear which columns the filters cover.
Covered by a new e2e case — a row with columns on two different websites asserts the spf param rides along only on the authoring website's requests. Verified it fails (3 leaked requests) with the previous row-wide scoping and passes with the fix.
A mixed board resolves entities per column, so a row can hold columns for different websites — or for pixels and links, which have no session properties or segments at all. Scoping the whole row meant filters authored against one website were sent with every column's query, so a sibling column reported empty or wrong numbers. Row filters now record the website they were defined against, and only the columns showing that website are scoped; the rest render unfiltered as before. Rows that mix entities show the website name next to the filter chips, so it is clear which columns the filters cover.
The chip strip borrowed the FilterBar's look without its behaviour: same bordered chips, but no remove buttons, because row filters belong to the board definition rather than the viewer's session. It also cost every filtered row a band of vertical space that unfiltered rows did not pay, so rows stopped lining up, and it wrapped on narrow viewports. Row filters resolve per column, so the row was the wrong place for it anyway. BoardRowFilterIndicator now sits in each affected panel's header next to the entity badge: a funnel, the filter's value inline so a board of per-region rows is scannable without hovering, and the full set in a tooltip. Only the scoped columns carry it, which is what made the mixed-row entity badge necessary before — that special case is gone. While editing, the row's filter button turns primary when the row carries filters, so it stays discoverable with nothing above the row. No behaviour change to what gets filtered; the e2e mixed-row test now also asserts exactly one column is marked.
Closes #4468.
Problem
Boards can't carry filters, and in particular can't carry different filters per row:
Filterbutton writes into the URL, so pressing Save doesn't keep it — reload and the filter is gone.useFilterParameters(), which reads only the URL, so all rows necessarily share one filter set.That makes the layout people keep asking for impossible to express: one row per segment/property value — e.g. a row of metrics per region, per plan tier, per app version — side by side in one board.
This is complementary to #4233 / #4303, not a replacement: that persists one filter set per board in localStorage for the current viewer. This makes the filters part of the board definition, so every viewer (and every share link) sees the composition the author built.
Approach
Row filters live on
BoardRow.filtersinsideboard.parameters. No migration: the column is JSON and the update schema already passesparametersthrough untouched.FilterScopeProvidersupplies extra filter params to everything below it, anduseFilterParameters()merges them in — so every existing query hook picks row filters up without a change of its own.mergeFilterParams()renumbers colliding params (path→path1,spf0→spf1) so a row filter narrows the board's filter instead of silently replacing it.allowFilter: false, since they belong to the board definition rather than the viewer's selection.FilterEditForm(now seedable throughdefaultValues, since it otherwise reads the URL). Both design and view mark each scoped panel with a read-only filter indicator in its header — a funnel, the filter's value inline, and the full set on hover — so filtered rows keep the same height as unfiltered ones.Group, so layout and resizing are untouched.Event property filters are deliberately out of scope: the dialog only offers them on
/events, which no board component renders.Tests
src/lib/params.test.ts—mergeFilterParamscollision/renumbering,boardRowFiltersToParamsserialization.src/components/hooks/useFilterParameters.test.ts— scope merging, scopedsegment/cohort/matchprecedence, and that scoped filters surviveallowFilter: false.tests/e2e/board-row-filters.spec.ts— a row filter survives the save/reload round trip, and scopes only its own row (spfparam present for the filtered row, absent for an unfiltered one).Verified by hand too: set
user_region is VastraGotalandon one row of a board whose website has 6 sessions (3 per region), saved, reloaded — the indicator persists and that row reports 3 visitors while an unfiltered row above it still reports 6.Note
The e2e spec passes
description: ''when creating boards becausePOST /api/boards500s without it — fixed separately in #4469.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.