Skip to content

feat(cost): filter rail, provider marks and drill-down on the Cost page - #2548

Merged
Makisuo merged 1 commit into
mainfrom
cost/filter-rail-and-marks
Nov 12, 2026
Merged

Makisuo merged 1 commit into
mainfrom
cost/filter-rail-and-marks

Conversation

@Makisuo

@Makisuo Makisuo commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

What

The Cost page was hard to explore: filtering only happened by clicking a table row, connected accounts showed as bare ids (465760687006, user_2zZ0…), nothing showed what a key was, and the Usage type grouping wasn't useful.

  • Filter rail, the same one the other list pages use: one checkbox section per dimension (Provider, Connected account, Service, Category, Region). Each value shows its mark and its cost, long lists are searchable, and there is Only and Clear all. Each section reads under every filter but its own, so ticking OpenRouter still offers AWS under Provider.
  • Marks everywhere (table, chart legend, filter chips, rail): AWS services and regions get the AWS logo (Lambda its own), OpenRouter models their vendor's logo, categories a glyph, global a globe.
  • Connected accounts say what they are: "AWS 465760687006", and "OpenRouter" with the raw id muted beside it (the id stays in the label when an org has two OpenRouter accounts, so their series stay apart). AWS regions show their place ("eu-central-1 · Frankfurt").
  • Row click drills down: it filters and regroups to the next dimension (service → region, provider/account/category/region → service), skipping one already narrowed. It used to filter and keep the grouping, which left one row.
  • Usage type leaves the grouping control and the URL. The data underneath is unchanged.

Kit additions: an optional mark on legend series (BarChart seriesMarks) and on active filter chips, formatCount on filterSection, and modelSlugIcon in ai-vendor.

Cost of the rail

The rail's values come from one breakdown read per dimension (5 reads, run side by side, limit 100 each) on every load and filter change.

Not in this PR

  • Exclude on rail rows: the cost API only takes include filters, so it needs an API and query change first.

Screenshots (/lab/cost, dark)

Before After
before service after service
before account after account

Verification

  • tsrs --noEmit (apps/web, packages/views), oxlint with .oxlintrc.effect.json, ui:patterns:check
  • Vitest: src/page/cost, src/lab, src/route/route.test.ts, packages/views charts and filters. New marks.test.ts covers account labels, marks, region names, facet filters and toggle/Only. The row-link scene test now asserts the drill-down.

Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.

The Cost page gets the filter rail the other list pages use: one checkbox
section per dimension (provider, connected account, service, category,
region), each value with its mark and its cost, searchable where long, with
Only and Clear all. Each section reads under every filter but its own, so
its other values stay on offer.

Every key carries a mark in the table, the chart legend and the filter
chips: AWS services and regions the AWS logo, OpenRouter models their
vendor's, categories a glyph. A connected account reads as the provider
that bills it ("AWS 4657...", "OpenRouter") instead of a bare id, and an
AWS region names its place.

A row click now filters and regroups to the next dimension (service to
region, provider/account/category/region to service), skipping one already
narrowed. Usage type leaves the grouping control and the URL.
@coderabbitai

coderabbitai Bot commented Nov 12, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 6 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 14e5b138-cb95-4d4c-8d5f-965cb66a8dfa
Commits

Reviewing files that changed from the base of the PR and between 2ab47f5 and 0b4b4eb.

Files selected for processing: 20

  • apps/web/src/lab/cost-fixture.ts
  • apps/web/src/lab/cost-lab.ts
  • apps/web/src/page/cost/command.ts
  • apps/web/src/page/cost/cost.scene.test.ts
  • apps/web/src/page/cost/derive.ts
  • apps/web/src/page/cost/marks.test.ts
  • apps/web/src/page/cost/marks.ts
  • apps/web/src/page/cost/message.ts
  • apps/web/src/page/cost/model.ts
  • apps/web/src/page/cost/update.ts
  • apps/web/src/page/cost/view-chart.ts
  • apps/web/src/page/cost/view-filters.ts
  • apps/web/src/page/cost/view-table.ts
  • apps/web/src/page/cost/view.ts
  • apps/web/src/route/search.ts
  • packages/views/src/charts/bar-chart.ts
  • packages/views/src/charts/timeseries-legend.ts
  • packages/views/src/lib/ai-vendor.ts
  • packages/views/src/ui/filters/active-filter-chips.ts
  • packages/views/src/ui/filters/filter-section.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@maple-review-bot

maple-review-bot Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Maple review

🟡 Confidence 6/10 · needs attention. Held at 6 because a correctness warning is open.
1 warning −1 · 3 notes · tests partial −1 · risk medium

Moves the /cost filters into the shared rail, adds provider marks in the table, legend, chips and rail, and makes a row click regroup instead of keeping the grouping. The rail ships a dead Only action, and a few labels and kit reads need tightening.

  • FilterSection gains formatCount; BarChart gains seriesMarks; chips gain mark
  • FetchCostFacets reads one breakdown per dimension under the other dimensions' filters
  • Row clicks regroup via drilledInto/drillTarget; usage_type leaves the URL and grouping
  • marks.ts names AWS/OpenRouter accounts and picks a mark per dimension

Findings

🟠 Warning · F1 · onOnly without onExclude renders no Only button, so the rail has none

correctness · apps/web/src/page/cost/view-filters.ts:69

filter-section.ts:270 draws the Only/Exclude hover overlay only when onExclude is set, and this section passes onOnly alone. No rail row ever shows Only: Message.ClickedOnlyFacetValue is never sent and onlyFilter is dead. Every other rail (services/list/view-filters.ts:39) passes both.

Pass an `onExclude` too, or in `filter-section.ts:270` render the overlay when either action exists: `...(config.onExclude === undefined && config.onOnly === undefined ? [] : [...])`.
🔵 Note · F2 · keyDetail repeats the id accountLabel already appended

correctness · apps/web/src/page/cost/marks.ts:146-148

With two OpenRouter sources accountLabel returns OpenRouter org_second (marks.ts:95), and keyDetail returns the id unconditionally, so the table row and the rail's title read OpenRouter org_second · org_second.

Return `key` only while the label is the bare name: `accountLabel(key, sources) === COST_PROVIDERS.openrouter.name ? key : null`.
🔵 Note · F3 · One facet key re-reads all five breakdowns on every filter change

performance · apps/web/src/page/cost/update.ts:124-128

requestKey covers the whole facets input, so a checkbox click re-runs all five FetchCostFacets reads (command.ts:104). facetFilters leaves the toggled dimension out of its own read, so only four of them changed and the fifth is re-read unchanged on every click.

Key and store each dimension's facet on its own (`costFacet.${dimension}` plus a `Keyed` slot), so the untouched one is reused.
🔵 Note · F4 · seriesMarks indexed without Rec.has, so a constructor series crashes the legend

correctness · packages/views/src/charts/bar-chart.ts:88

A series column named constructor reads back Object.prototype.constructor, so mark is not undefined, mark.icon is undefined, and the legend's icon(h, undefined) throws instead of drawing a plain entry. marks.ts:57 and ai-vendor.ts:68 guard exactly this read with Rec.has.

`mark: Rec.has(inputs.seriesMarks, definition.rawKey) ? inputs.seriesMarks[definition.rawKey] : undefined`
🤖 Prompt to fix all 4 findings with an AI agent
Findings from an automated review of commit 0b4b4eb79ca4d861a2740224d595b78838c2dc5f. Verify each one against the current code before changing anything, fix only those that still apply, and keep each fix to the lines it names.

---

F1 · Warning · correctness · apps/web/src/page/cost/view-filters.ts:69
`onOnly` without `onExclude` renders no Only button, so the rail has none
`filter-section.ts:270` draws the Only/Exclude hover overlay only when `onExclude` is set, and this section passes `onOnly` alone. No rail row ever shows Only: `Message.ClickedOnlyFacetValue` is never sent and `onlyFilter` is dead. Every other rail (`services/list/view-filters.ts:39`) passes both.
Suggested fix: Pass an `onExclude` too, or in `filter-section.ts:270` render the overlay when either action exists: `...(config.onExclude === undefined && config.onOnly === undefined ? [] : [...])`.

---

F2 · Note · correctness · apps/web/src/page/cost/marks.ts:146-148
`keyDetail` repeats the id `accountLabel` already appended
With two OpenRouter sources `accountLabel` returns `OpenRouter org_second` (`marks.ts:95`), and `keyDetail` returns the id unconditionally, so the table row and the rail's title read `OpenRouter org_second · org_second`.
Suggested fix: Return `key` only while the label is the bare name: `accountLabel(key, sources) === COST_PROVIDERS.openrouter.name ? key : null`.

---

F3 · Note · performance · apps/web/src/page/cost/update.ts:124-128
One facet key re-reads all five breakdowns on every filter change
`requestKey` covers the whole `facets` input, so a checkbox click re-runs all five `FetchCostFacets` reads (`command.ts:104`). `facetFilters` leaves the toggled dimension out of its own read, so only four of them changed and the fifth is re-read unchanged on every click.
Suggested fix: Key and store each dimension's facet on its own (`costFacet.${dimension}` plus a `Keyed` slot), so the untouched one is reused.

---

F4 · Note · correctness · packages/views/src/charts/bar-chart.ts:88
`seriesMarks` indexed without `Rec.has`, so a `constructor` series crashes the legend
A series column named `constructor` reads back `Object.prototype.constructor`, so `mark` is not undefined, `mark.icon` is undefined, and the legend's `icon(h, undefined)` throws instead of drawing a plain entry. `marks.ts:57` and `ai-vendor.ts:68` guard exactly this read with `Rec.has`.
Suggested fix: `mark: Rec.has(inputs.seriesMarks, definition.rawKey) ? inputs.seriesMarks[definition.rawKey] : undefined`

0b4b4eb · What was checked, coverage and telemetry details are on the Maple / review check. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@Makisuo
Makisuo merged commit 55baa25 into main Nov 12, 2026
27 checks passed
@Makisuo
Makisuo deleted the cost/filter-rail-and-marks branch November 12, 2026 23:50

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 inline note from Maple's review. The score and summary are in the review comment above.

sections: model.filterSections,
toSectionsMessage: toSections,
onToggle: (value) => Message.ToggledFacetValue({ dimension, value }),
onOnly: (value) => Message.ClickedOnlyFacetValue({ dimension, value }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

onOnly without onExclude renders no Only button, so the rail has none

F1 · Warning · correctness

filter-section.ts:270 draws the Only/Exclude hover overlay only when onExclude is set, and this section passes onOnly alone. No rail row ever shows Only: Message.ClickedOnlyFacetValue is never sent and onlyFilter is dead. Every other rail (services/list/view-filters.ts:39) passes both.

Pass an `onExclude` too, or in `filter-section.ts:270` render the overlay when either action exists: `...(config.onExclude === undefined && config.onOnly === undefined ? [] : [...])`.
🤖 Prompt to fix with an AI agent
In `apps/web/src/page/cost/view-filters.ts:69`: `onOnly` without `onExclude` renders no Only button, so the rail has none.

`filter-section.ts:270` draws the Only/Exclude hover overlay only when `onExclude` is set, and this section passes `onOnly` alone. No rail row ever shows Only: `Message.ClickedOnlyFacetValue` is never sent and `onlyFilter` is dead. Every other rail (`services/list/view-filters.ts:39`) passes both.

Suggested fix: Pass an `onExclude` too, or in `filter-section.ts:270` render the overlay when either action exists: `...(config.onExclude === undefined && config.onOnly === undefined ? [] : [...])`.

Verify the problem exists at that location before changing it, and keep the fix to those lines.

Makisuo added a commit that referenced this pull request Nov 13, 2026
Keeps the Cost page rework (#2548) as it landed and re-applies the provider on
top: the provider mark reads an entry without a tint, the lab's facet read
leaves out removed sources like its other reads, and the empty state names
Cloudflare beside the reworked sentence.
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