Skip to content

feat: per-row filters saved with a board - #4470

Open
brantsrasmus wants to merge 4 commits into
umami-software:devfrom
brantsrasmus:feat/board-row-filters
Open

brantsrasmus wants to merge 4 commits into
umami-software:devfrom
brantsrasmus:feat/board-row-filters

Conversation

@brantsrasmus

@brantsrasmus brantsrasmus commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Closes #4468.

Problem

Boards can't carry filters, and in particular can't carry different filters per row:

  1. The board Filter button writes into the URL, so pressing Save doesn't keep it — reload and the filter is gone.
  2. Every board component resolves its filters from 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.filters inside board.parameters. No migration: the column is JSON and the update schema already passes parameters through untouched.

  • FilterScopeProvider supplies extra filter params to everything below it, and useFilterParameters() merges them in — so every existing query hook picks row filters up without a change of its own.
  • mergeFilterParams() renumbers colliding params (pathpath1, spf0spf1) 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 belong to the board definition rather than the viewer's selection.
  • Board design gains a per-row filter button reusing FilterEditForm (now seedable through defaultValues, 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.
  • Rows without filters render exactly as before — no extra wrapper around the resizable 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.tsmergeFilterParams collision/renumbering, boardRowFiltersToParams serialization.
  • src/components/hooks/useFilterParameters.test.ts — scope merging, scoped segment/cohort/match precedence, and that scoped filters survive allowFilter: false.
  • tests/e2e/board-row-filters.spec.ts — a row filter survives the save/reload round trip, and scopes only its own row (spf param present for the filtered row, absent for an unfiltered one).

Verified by hand too: set user_region is VastraGotaland on 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 because POST /api/boards 500s without it — fixed separately in #4469.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

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).
@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

@brantsrasmus is attempting to deploy a commit to the Umami Software Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c83dd0e7-9a47-4565-954a-fc8e76c986f9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR stores structured filters on individual board rows and injects them into descendant analytics queries through a new filter-scope context.

  • Adds row-level filter editing and read-only filter chips to board design and view modes.
  • Adds collision-safe filter-parameter merging and serialization helpers.
  • Extends filter-form initialization and adds unit and end-to-end coverage.
  • Mixed-entity rows currently apply website-specific filters indiscriminately to every column.

Confidence Score: 4/5

The 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

Important Files Changed

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]
Loading

Reviews (1): Last reviewed commit: "test: cover board row filters end to end" | Re-trigger Greptile

}

return (
<FilterScopeProvider params={scopeParams}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
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