Skip to content

feat(billing): Paginate invoice comparison admin UI#116647

Merged
armcknight merged 7 commits into
masterfrom
andrewmcknight/reveng-131-paginate-invoice-comparison
Jun 3, 2026
Merged

feat(billing): Paginate invoice comparison admin UI#116647
armcknight merged 7 commits into
masterfrom
andrewmcknight/reveng-131-paginate-invoice-comparison

Conversation

@armcknight
Copy link
Copy Markdown
Member

@armcknight armcknight commented Jun 1, 2026

Summary

REVENG-131. Paginates the /_admin/cells/$region/invoice-comparison/ page so operators can navigate past the first 200 rows.

  • Both tables (both-sides comparison + one-sided unmatched debug) get their own paginator. The two paginators are independent: paging through rows doesn't move you in unmatched, and vice versa.
  • Page numbers live in URL search params (rows_page, unmatched_page) so refresh and share-links keep their position.
  • Running a new comparison resets both paginators to page 1 — the new window's result set is unrelated to the previous one's page numbers.
  • Drops the inline "showing top N" notes from the panel headers; replaces them with a paginator row showing start–end of total · page X of Y plus prev/next.

Backend PR: https://github.com/getsentry/getsentry/pull/20496

image

Test plan

  • With getsentry#20496 deployed locally and make seed-invoice-comparison run, load /_admin/cells/<region>/invoice-comparison/, click Run, and confirm both tables show page 1 of 2 by default (page_size=200, 250 rows each)
  • Click Next on the rows paginator — URL updates with ?rows_page=2, rows change, unmatched paginator stays on page 1
  • Click Next on the unmatched paginator — URL updates with ?unmatched_page=2, unmatched rows change, rows table stays on page 2
  • Refresh — both paginators stay on page 2
  • Change the date window and click Run — both paginators reset to page 1 (URL ?rows_page=1&unmatched_page=1)
  • Empty window (e.g. start = end = now) — paginator row shows "No rows" rather than a broken Prev/Next pair
  • Manually visit ?rows_page=99 — backend clamps to last page, frontend shows that page with Next disabled

🤖 Generated with Claude Code

@linear-code
Copy link
Copy Markdown

linear-code Bot commented Jun 1, 2026

REVENG-131

@github-actions github-actions Bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Jun 1, 2026
@armcknight armcknight marked this pull request as draft June 1, 2026 22:47
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

📊 Type Coverage Diff

✅ no issues found

Comment thread static/gsAdmin/views/invoiceComparison.tsx Outdated
@armcknight armcknight force-pushed the andrewmcknight/reveng-131-paginate-invoice-comparison branch from 2abb8f5 to f810a8b Compare June 2, 2026 19:53
armcknight pushed a commit that referenced this pull request Jun 2, 2026
Address Cursor Bugbot feedback on #116647:

- Replace the `PaginatorRow` styled('div') with `<Flex justify="between"
  align="center" padding="md lg" borderBottom="primary">`. The styled
  rule mapped 1:1 to existing Flex props, and `static/AGENTS.md`
  prefers the layout primitive over new styled components.

- Type `PAGE_SIZE_OPTIONS` as `readonly number[]` instead of using
  `as const` + a call-site `as readonly number[]` cast. Same readonly
  guarantee, no widening cast, no new type-safety regression flagged
  by the type-coverage diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@armcknight armcknight marked this pull request as ready for review June 2, 2026 20:26
@armcknight armcknight force-pushed the andrewmcknight/reveng-131-paginate-invoice-comparison branch from f810a8b to 4a47cd5 Compare June 2, 2026 20:28
armcknight pushed a commit that referenced this pull request Jun 2, 2026
Address Cursor Bugbot feedback on #116647:

- Replace the `PaginatorRow` styled('div') with `<Flex justify="between"
  align="center" padding="md lg" borderBottom="primary">`. The styled
  rule mapped 1:1 to existing Flex props, and `static/AGENTS.md`
  prefers the layout primitive over new styled components.

- Type `PAGE_SIZE_OPTIONS` as `readonly number[]` instead of using
  `as const` + a call-site `as readonly number[]` cast. Same readonly
  guarantee, no widening cast, no new type-safety regression flagged
  by the type-coverage diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread static/gsAdmin/views/invoiceComparison.tsx
armcknight pushed a commit that referenced this pull request Jun 2, 2026
Address Cursor Bugbot feedback on #116647:

- Replace the `PaginatorRow` styled('div') with `<Flex justify="between"
  align="center" padding="md lg" borderBottom="primary">`. The styled
  rule mapped 1:1 to existing Flex props, and `static/AGENTS.md`
  prefers the layout primitive over new styled components.

- Type `PAGE_SIZE_OPTIONS` as `readonly number[]` instead of using
  `as const` + a call-site `as readonly number[]` cast. Same readonly
  guarantee, no widening cast, no new type-safety regression flagged
  by the type-coverage diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@armcknight armcknight force-pushed the andrewmcknight/reveng-131-paginate-invoice-comparison branch from 4a47cd5 to 179b787 Compare June 2, 2026 23:21
Comment thread static/gsAdmin/views/invoiceComparison.tsx
armcknight pushed a commit that referenced this pull request Jun 2, 2026
Address Cursor Bugbot feedback on #116647:

- Replace the `PaginatorRow` styled('div') with `<Flex justify="between"
  align="center" padding="md lg" borderBottom="primary">`. The styled
  rule mapped 1:1 to existing Flex props, and `static/AGENTS.md`
  prefers the layout primitive over new styled components.

- Type `PAGE_SIZE_OPTIONS` as `readonly number[]` instead of using
  `as const` + a call-site `as readonly number[]` cast. Same readonly
  guarantee, no widening cast, no new type-safety regression flagged
  by the type-coverage diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@armcknight armcknight force-pushed the andrewmcknight/reveng-131-paginate-invoice-comparison branch from 179b787 to 35764db Compare June 2, 2026 23:37
Comment thread static/gsAdmin/views/invoiceComparison.tsx
Comment thread static/gsAdmin/views/invoiceComparison.tsx Outdated
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1dfd234. Configure here.

Comment thread static/gsAdmin/views/invoiceComparison.tsx
armcknight pushed a commit that referenced this pull request Jun 3, 2026
Address Cursor Bugbot feedback on #116647:

- Replace the `PaginatorRow` styled('div') with `<Flex justify="between"
  align="center" padding="md lg" borderBottom="primary">`. The styled
  rule mapped 1:1 to existing Flex props, and `static/AGENTS.md`
  prefers the layout primitive over new styled components.

- Type `PAGE_SIZE_OPTIONS` as `readonly number[]` instead of using
  `as const` + a call-site `as readonly number[]` cast. Same readonly
  guarantee, no widening cast, no new type-safety regression flagged
  by the type-coverage diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@armcknight armcknight force-pushed the andrewmcknight/reveng-131-paginate-invoice-comparison branch from bf57700 to caedb65 Compare June 3, 2026 17:50
Andrew McKnight and others added 7 commits June 3, 2026 10:29
Adds page navigation to `/_admin/cells/$region/invoice-comparison/`. Each
of the page's two tables (the both-sides comparison list and the
one-sided unmatched debug list) gets its own paginator backed by URL
search params (`rows_page` / `unmatched_page`) so refresh and
shareable URLs preserve position. Running a new comparison resets both
pages to 1.

Consumes the new `summary.{rows,unmatched}_{page,page_size,total_pages}`
contract from getsentry#20496.

REVENG-131.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…omparison

Two fixes + one feature:

- Move `start` / `end` from local state into URL search params. Previously
  a refresh wiped the in-memory `submitted` value, which disabled the
  query and reset the page to the empty input form — and the same gap
  made `?rows_page=99` URLs render nothing. The window is now reread
  from the URL on mount and the query fires whenever start+end are
  present.

- After every response, if the server clamped the requested page (e.g.
  asked for page 99 against a 2-page result), replace the URL with the
  clamped value so refresh converges on the real page rather than
  re-requesting the bad one.

- Add a "Results per page" dropdown with 25 / 50 / 100 / 250 options.
  Selection persists in the URL as `page_size` and is applied to both
  tables (sent as `rows_page_size` and `unmatched_page_size` on the
  request). Default is 50. Changing the value resets both paginators
  to page 1 — the previous offset doesn't map cleanly to the resized list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address Cursor Bugbot feedback on #116647:

- Replace the `PaginatorRow` styled('div') with `<Flex justify="between"
  align="center" padding="md lg" borderBottom="primary">`. The styled
  rule mapped 1:1 to existing Flex props, and `static/AGENTS.md`
  prefers the layout primitive over new styled components.

- Type `PAGE_SIZE_OPTIONS` as `readonly number[]` instead of using
  `as const` + a call-site `as readonly number[]` cast. Same readonly
  guarantee, no widening cast, no new type-safety regression flagged
  by the type-coverage diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address Cursor Bugbot finding: the `start` / `end` `datetime-local`
inputs were initialized lazily once and then never updated, so browser
back/forward (or any in-app navigation that swapped the query params)
would leave the input fields showing a stale window while the fetched
results matched the new URL.

Add a `useEffect` that mirrors the URL window back into `startInput` /
`endInput` whenever those query params are present. When the URL has no
window yet, the effect skips the update so a user's in-progress typing
isn't clobbered before they submit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address Cursor Bugbot finding: `parsePageSizeParam` silently maps any
unsupported `page_size` query value (e.g. `?page_size=200`) to the
default while leaving the URL untouched. The result: the API and UI
ran at the resolved size while the address bar advertised something
else, so refresh / share-links did not reproduce the intended view —
unlike `rows_page` / `unmatched_page`, which were already corrected.

Add a small `useEffect` that, whenever the URL has a `page_size` value
that doesn't match the resolved one, replaces the URL with the
resolved value. Runs eagerly (no dependency on `data`) since the
validity check is purely client-side, so the address bar matches the
real page size before the first request even returns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…oads

Address Cursor Bugbot finding: clicking Prev / Next (or changing the
page-size dropdown) updated the React Query key, but the query didn't
keep prior data while the next page fetched. The summary and both
tables are gated on `{data && (…)}`, so the whole comparison view
collapsed to the loading indicator on every page change — surprisingly
janky for a paginator.

Pass `placeholderData: keepPreviousData` so the previously-rendered
page stays mounted until the new response arrives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…omparison

Address Cursor Bugbot finding: with `placeholderData: keepPreviousData`,
`data.summary` reflects the previously-rendered page while React Query
loads the next one. The clamp effect that snaps the URL to the
server-reported page therefore fired against stale summary data —
every Prev / Next click would rewrite the URL back to the prior page,
breaking pagination outright (and the same path broke clamped deep
links once the first page loaded).

Read `isPlaceholderData` from `useQuery` and bail out of the clamp
effect while it's true, so the URL only converges on the server's
authoritative page number once the new response actually lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@armcknight armcknight force-pushed the andrewmcknight/reveng-131-paginate-invoice-comparison branch from caedb65 to 299dc36 Compare June 3, 2026 18:29
@armcknight armcknight enabled auto-merge (squash) June 3, 2026 18:29
@armcknight armcknight merged commit c3c906b into master Jun 3, 2026
77 checks passed
@armcknight armcknight deleted the andrewmcknight/reveng-131-paginate-invoice-comparison branch June 3, 2026 18:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants