Skip to content

fix(admin): stop duplicate API calls across admin tables - #1869

Open
Shreyag02 wants to merge 20 commits into
mainfrom
fix/admin-duplicate-api-calls
Open

fix(admin): stop duplicate API calls across admin tables#1869
Shreyag02 wants to merge 20 commits into
mainfrom
fix/admin-duplicate-api-calls

Conversation

@Shreyag02

@Shreyag02 Shreyag02 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Every server-mode table in the admin UI was fetching its first page twice, and the organization detail page could fetch its active tab up to four times. Both were visible on staging as (canceled) requests in the network tab.

The root cause is small: our tables pass defaultSort to DataTable but leave sort out of the initial query. DataTable merges defaultSort in and emits it on mount, which changes the connect-query cache key and triggers a second request. The first one is then aborted mid-flight — after the server has already done the work.

This PR is scoped to duplicate and redundant requests only. Measured on a production build: the organization list now issues 5 requests with no cancellation, against 6-with-a-cancelled-SearchOrganizations on the deployed build. Numbers below.

Changes

Change What was happening Scope
Seed the initial query with sort DataTable's mount emit changed the request, so every table fetched page 1 twice All 11 server tables
Drop the empty defaultSort on project members Sent a sort with an empty field name; that endpoint ignores sort entirely Project members dialog
Keep the org detail tab mounted while billing loads The tab unmounted and remounted mid-load, replaying every request in it details/index.tsx
Cover both billing legs in isBillingAccountLoading Consequence of the row above: the flag missed the listBillingAccounts leg, so the side panel rendered empty fallbacks as settled values Org context, side panel, edit billing, add tokens
Latch "load more" against scroll bursts Repeat calls cancel the in-flight page and gain nothing — and a render-scoped guard can't stop them All 11 server tables
Scope the members invalidation to its own org An empty input matched partially, invalidating every org's cached member list Members tab
Add a default staleTime of 30s staleTime: 0 + refetchOnMount refetched reference data on every navigation App-wide
Reuse the org resolved from a slug URL The same org was fetched again by id under a different cache key Org detail page
Fetch the org member map only in the projects tab The full member list was fetched on every org page, read by one tab Org context → projects tab
Gate the invite dialog's queries on open Its trigger sits in the navbar, so searchOrganizations + listRoles ran on every visit to the users list Users list
Disable Add tokens without a billing account Fix to this PR: the submit guard it added returns silently, so for a billing-less org the button looked live and the click did nothing Add tokens dialog
Log member-map fetch failures again Fix to this PR: moving the query into a hook dropped the provider's console.error, leaving the projects tab rendering raw user ids with no signal Projects tab

Technical Details

Why the key changes. connect-query builds cache keys with createMessageKey, which omits unset fields. sort: [] and sort: [{name: "created_at", …}] therefore hash differently — same query in our heads, two cache entries in practice. Seeding the initial sort makes the mount emit structurally identical to what's already in state, so the key never changes.

Why the tab remounted. The layout's isLoading included isBillingAccountLoading, but the listBillingAccounts call that enables that query wasn't in the gate. A disabled query reports isLoading: false, so:

Step Gate Result
Org + roles still loading true Spinner
Org + roles settled, billing not yet enabled false Tab mounts, tables fetch
listBillingAccounts resolves, billing query enables true Tab unmounts
Billing settles false Tab remounts, tables fetch again

The gate now only includes queries enabled from the first render, so it can flip once and stay there.

What narrowing the gate exposed. The same quirk also made isBillingAccountLoading wrong for its own consumers: it covers getBillingAccount, which stays disabled until listBillingAccounts yields an id. While the spinner hid that window nobody noticed. With the tab mounted throughout, the side panel rendered N/A for billing name/email/address and 0 / Prepaid for tokens as though they were settled, and Edit billing / Add tokens became submittable before the ids existed. The context now ORs both legs, and the side-panel sections wait on the combined flag. Both submits are guarded — but a guard alone was wrong for Add tokens: with billing settled and no account the flag is false, so the button enabled and the guard swallowed the click silently. It now disables on the same condition the guard rejects. Edit billing keeps the guard alone; that panel isn't reachable in the no-billing state.

Why a guard wasn't enough for load-more. hasNextPage and isFetchingNextPage are last-render values, and react-query notifies observers on a macrotask. VirtualizedContent calls onLoadMore straight from onScroll, so a burst of scroll events clears the guard several times before React re-renders — and fetchNextPage defaults to cancelRefetch: true, aborting the in-flight page and re-issuing it. A ref that flips synchronously is what closes it; the guard stays as a cheap first filter.

Two changes that only work together. Seeding the resolved org into the cache does nothing while staleTime is 0 — the entry is stale on arrival and the view refetches anyway. Please don't land one without the other:

Setup Requests
Not seeded, staleTime 30s 1
Seeded, staleTime 0 1
Seeded, staleTime 30s 0

The seed writes only into an empty key. The slug-keyed and id-keyed entries are separate, and an org edit invalidates only the id-keyed one, so an unconditional seed could put a stale slug copy over fresher data. A UUID URL skips the seed entirely: the resolve already owns that key.

Merged main

main moved under this branch (metaschema reconcile, server panic recovery, pending-invites work). One conflict, in details/index.tsx, where both sides reworked the same block of the provider:

Side Change Kept
main Inline listRoles + listOrganizationRoles replaced with a useOrganizationRoles hook Yes — the hook logs both role errors itself, so dropping them from the provider's error effect loses no logging
This branch Member map removed from the provider Yes — the query and its error log now live in useOrgMembersMap and the projects tab, its only readers
Both isLoading Combined: isOrganizationLoading || isRolesLoading. main had the billing leg; this branch dropped it. Keeping either side verbatim would regress one of the two

main's new invites table needs nothing from this PR: it's mode="client", so Apsara skips the mount emit entirely (gated on mode === 'server'), and it has no load-more path to latch.

Not in this PR

Structural cleanups found along the way — a shared useServerTableQuery hook, error handling, dead code — are queued separately on fix/admin-ui-followups; none of them are needed for this fix.

One caveat worth knowing: narrowing the loading gate makes a pre-existing edit-KYC bug more likely to surface. Until the one-hop KYC query lands, opening Edit KYC on a verified org can show it as unverified, and saving writes the verification away. It's tracked on CLD-802 and fixed on that branch.

Also out of scope: staging shows a GetOrganization returning 403, reproducible on a local production build. It isn't a duplicate-call artifact — app/organization#get grants superusers access only via platform->superuser, which needs the org's platform relation tuple, written once by AttachToPlatform at creation with no backfill path. It surfaces on the admins list, where OrgCell issues one GetOrganization per service-user row (views/admins/columns.tsx:74-78); the affected row falls back to the raw id with its button disabled, so nothing breaks. Raising separately.

Test Plan

  • Build and type checking passes
  • Manual testing on a production build — organization list and in-app org detail
  • Cold load and fast-scroll still to check
Check Result
pnpm build in web/sdk, pnpm build in web/apps/admin Succeed
tsc --noEmit Same 21 pre-existing errors before and after, and unchanged by the main merge; none in touched files. web/apps/admin is clean
go build ./... and go vet after the merge Clean
Every mode="server" table audited 11 in web/sdk/admin, all 11 covered here; each pairs seeded sort with defaultSort, and project members correctly has neither
eslint on all changed files 0 errors; only pre-existing warnings
Cache key, replaying real Apsara + connect-query code Keys differ before the change, match after
Cache seeding vs staleTime Seeded + staleTime: 0 → 1 request; seeded + 30s → 0 requests

Measured on a production build (vite preview; dev is unusable for this — React.StrictMode double-invokes mounts and connect-query aborts and refires each query, so dev numbers roughly double):

Page Requests Before
/organizations cold load GetCurrentUser, configs, GetCurrentAdminUser, SearchOrganizations, ListPlans5, all 200, none cancelled 6, with a cancelled SearchOrganizations
Users, audit logs, invoices, roles, products, webhooks, preferences, admins Exactly one request each
Org detail via in-app nav One GetOrganization, then the detail fan-out; one SearchOrganizationUsers, no remount Tab remounted, replaying its table query

Still to check:

# Check Expected
1 Org detail cold load — hard refresh on /organizations/<slug>/security One GetOrganization, not two. In-app nav carries state.orgId and skips the resolve, so it does not exercise the seed
2 Fast-scroll load-more on the org list One request per page, none cancelled
3 Sort, filter and infinite scroll on each table Unchanged behaviour, one request per change
4 Projects tab Member avatars render; add-members dropdown still filters
5 Users list → Invite No requests until the dialog opens; pickers populate once it does
6 Org side panel during load Skeletons, not N/A / 0 / Prepaid

@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview Aug 19, 2026 6:11am

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 37b5902e-c14d-442e-b20d-2b475db166ef

📥 Commits

Reviewing files that changed from the base of the PR and between 5ff0902 and 2cfe257.

📒 Files selected for processing (4)
  • web/sdk/admin/views/organizations/details/index.tsx
  • web/sdk/admin/views/organizations/details/layout/add-tokens-dialog.tsx
  • web/sdk/admin/views/organizations/details/members/index.tsx
  • web/sdk/admin/views/organizations/details/projects/index.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • web/sdk/admin/views/organizations/details/members/index.tsx
  • web/sdk/admin/views/organizations/details/index.tsx
  • web/sdk/admin/views/organizations/details/projects/index.tsx
  • web/sdk/admin/views/organizations/details/layout/add-tokens-dialog.tsx

Included review availability: Your plan includes up to 2 reviews per rolling hour; 1 remains after this review.


📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Prevented duplicate or overlapping pagination requests across organization, user, audit log, invoice, token, and project views.
    • Improved loading indicators for billing details, token usage, and account data.
    • Prevented billing and token actions when required account information is unavailable.
    • Corrected organization lookup behavior for authorized administrators.
    • Improved table sorting consistency when pages first load.
  • Performance

    • Improved data freshness and reduced unnecessary requests.
    • Invitation-related data now loads only when the invitation dialog is open.
    • Improved member data reuse across organization and project screens.

Walkthrough

The PR updates React Query freshness and organization cache priming, moves member-map loading into a dedicated hook, prevents overlapping table pagination requests, aligns initial table sorting, and updates billing and invitation query loading behavior.

Changes

Admin query and organization data updates

Layer / File(s) Summary
Query freshness and organization cache
web/apps/admin/src/contexts/ConnectProvider.tsx, web/apps/admin/src/pages/organizations/details/index.tsx
The shared query client uses a 30-second stale time. Slug-resolved organizations prime their ID-based cache entries.
Organization member data ownership
web/sdk/admin/hooks/useOrgMembersMap.ts, web/sdk/admin/views/organizations/details/...
Organization member lookup uses useOrgMembersMap. The organization context no longer exposes member-map data.
Table query and pagination coordination
web/sdk/admin/views/audit-logs/index.tsx, web/sdk/admin/views/invoices/index.tsx, web/sdk/admin/views/organizations/..., web/sdk/admin/views/users/list/...
Initial sort values match DataTable queries. Pagination handlers reject unavailable, active, errored, or overlapping requests.
Conditional queries and billing loading states
web/sdk/admin/views/organizations/details/..., web/sdk/admin/views/users/list/invite-users.tsx
Billing loading state covers account discovery and detail fetching. Billing actions require required identifiers. Invitation queries run only while the dialog is open.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to 2cfe2

This change removes redundant admin requests and prevents billing actions from appearing ready before required data exists; no actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

@Shreyag02 Shreyag02 added the Do not merge Label to indicate that the PR is not ready to be merged even though might be (or not) approvals. label Aug 10, 2026
@Shreyag02
Shreyag02 marked this pull request as draft August 10, 2026 23:10
@Shreyag02 Shreyag02 changed the title Fix/admin duplicate api calls fix(admin): stop duplicate API calls across admin tables Aug 10, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a49db67e-1c52-40d1-9e9f-dff6cca3a8b0

📥 Commits

Reviewing files that changed from the base of the PR and between 8e22bcf and 8cc1965.

📒 Files selected for processing (21)
  • web/apps/admin/src/contexts/ConnectProvider.tsx
  • web/apps/admin/src/pages/organizations/details/index.tsx
  • web/sdk/admin/hooks/useOrgMembersMap.ts
  • web/sdk/admin/hooks/useServerTableQuery.ts
  • web/sdk/admin/views/admins/columns.tsx
  • web/sdk/admin/views/audit-logs/index.tsx
  • web/sdk/admin/views/audit-logs/navbar.tsx
  • web/sdk/admin/views/audit-logs/util.ts
  • web/sdk/admin/views/invoices/index.tsx
  • web/sdk/admin/views/organizations/details/apis/index.tsx
  • web/sdk/admin/views/organizations/details/contexts/organization-context.tsx
  • web/sdk/admin/views/organizations/details/index.tsx
  • web/sdk/admin/views/organizations/details/invoices/index.tsx
  • web/sdk/admin/views/organizations/details/members/index.tsx
  • web/sdk/admin/views/organizations/details/pat/index.tsx
  • web/sdk/admin/views/organizations/details/projects/index.tsx
  • web/sdk/admin/views/organizations/details/projects/members/index.tsx
  • web/sdk/admin/views/organizations/details/projects/use-add-project-members.tsx
  • web/sdk/admin/views/organizations/details/tokens/index.tsx
  • web/sdk/admin/views/organizations/list/index.tsx
  • web/sdk/admin/views/users/list/list.tsx
💤 Files with no reviewable changes (2)
  • web/sdk/admin/views/audit-logs/util.ts
  • web/sdk/admin/views/organizations/details/contexts/organization-context.tsx

/** Search owned outside the table, e.g. the organization page's shared box. */
search?: string;
/** Adjust the query before it becomes a request, e.g. converting units. */
mapQuery?: (query: DataTableQuery) => DataTableQuery;

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 2 'no-unused-vars|argsIgnorePattern|varsIgnorePattern' \
  -g 'eslint.config.*' -g '.eslintrc*' -g 'package.json' .

Repository: raystack/frontier

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- target file ---'
sed -n '1,100p' web/sdk/admin/hooks/useServerTableQuery.ts

printf '%s\n' '--- repository lint/config files ---'
git ls-files | rg '(^|/)(eslint\.config\.[^/]+|\.eslintrc[^/]*|package\.json|.*lint.*)$' | head -200

printf '%s\n' '--- unused-argument conventions ---'
rg -n -S 'argsIgnorePattern|varsIgnorePattern|no-unused-vars|unused.*(param|arg)|^ *[_$][A-Za-z0-9_]*[,:)]' \
  web package.json .github 2>/dev/null | head -300

Repository: raystack/frontier

Length of output: 4286


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- ESLint configuration ---'
cat -n web/.eslintrc.js
printf '%s\n' '--- shared ESLint configuration ---'
cat -n web/tools/eslint-config/index.js
printf '%s\n' '--- relevant package scripts and dependencies ---'
node - <<'JS'
const fs = require("fs");
for (const file of ["web/package.json", "web/sdk/package.json", "web/apps/admin/package.json"]) {
  if (!fs.existsSync(file)) continue;
  const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
  console.log(`--- ${file} ---`);
  console.log(JSON.stringify({
    scripts: pkg.scripts,
    eslintConfig: pkg.eslintConfig,
    devDependencies: pkg.devDependencies,
    dependencies: pkg.dependencies
  }, null, 2));
}
JS
printf '%s\n' '--- existing declaration-only parameter suppressions ---'
cat -n web/sdk/admin/components/PageHeader.tsx
rg -n -C 3 'eslint-disable.*no-unused-vars|callback param name|type documentation' web --glob '*.{js,jsx,ts,tsx}'

Repository: raystack/frontier

Length of output: 10156


Suppress the unused declaration parameter at line 20.

Add a targeted no-unused-vars suppression, consistent with web/sdk/admin/components/PageHeader.tsx. The query parameter at line 31 is used and does not need a suppression.

🧰 Tools
🪛 GitHub Check: JS SDK Lint

[warning] 20-20:
'query' is defined but never used

Source: Linters/SAST tools

Comment on lines +19 to +20
const { organization } = useContext(OrganizationContext);
const { data: orgMembersMap = {} } = useOrgMembersMap(organization?.id);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include organization member loading in the returned loading state.

If listProjectUsers resolves before useOrgMembersMap, eligibleMembers is empty while isLoading is false. The member picker can display an incorrect empty state.

Proposed fix
-  const { data: orgMembersMap = {} } = useOrgMembersMap(organization?.id);
+  const {
+    data: orgMembersMap = {},
+    isLoading: isOrgMembersMapLoading,
+  } = useOrgMembersMap(organization?.id);
...
-    isLoading,
+    isLoading: isLoading || isOrgMembersMapLoading,

@coveralls

coveralls commented Aug 10, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 32222363507

Coverage remained the same at 48.739%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 40091
Covered Lines: 19540
Line Coverage: 48.74%
Coverage Strength: 15.66 hits per line

💛 - Coveralls

Every server-mode DataTable fired two requests for its first page.

INITIAL_QUERY carried no sort while defaultSort was passed as a prop.
DataTable seeds its internal query from getDefaultTableQuery(defaultSort,
query) and its mount effect emits unconditionally, since oldQueryRef
starts null. The emitted query therefore differs from the one the parent
already had in state by exactly the sort field.

connect-query builds its cache key with createMessageKey, which omits
unset fields, so sort: [] and sort: [{...}] hash to different keys. The
key changed, a second request went out, and the first was aborted
mid-flight once its observer was dropped.

Seeding the initial sort makes the mount emit structurally identical to
the query already in state, so the key is unchanged and no refetch is
triggered.
The project members dialog passed defaultSort={{ name: "", order: "desc" }},
which sent an RQL sort with an empty field name on every mount and
guaranteed the key change that caused a duplicate request.

The sort was never applied: ProjectUsersRepository.prepareDataQuery builds
its statement from search, offset and limit only, and ignores sort
entirely. No column in this table is sortable either — title sets
enableSorting: false and the rest are unsorted.

Removing the prop leaves both the initial and emitted query at sort: [],
so ordering is unchanged and the mount no longer refetches.
The layout renders a spinner in place of its children while isLoading is
true, and isLoading included isBillingAccountLoading.

That query is gated on firstBillingAccountId, which arrives from a
separate listBillingAccounts call that was not itself in the gate. A
disabled query reports isLoading false, so once the org and role queries
settled the gate opened, the tab mounted and its tables fetched. When
listBillingAccounts then resolved, the billing query enabled, isLoading
went true again and the whole tab unmounted, only to remount and refetch
once billing settled.

Gating only on queries that are enabled from the first render makes the
transition monotonic, so the tab mounts once. The side panel already
renders its own skeletons while billing resolves.
VirtualizedContent calls loadMoreData() from its scroll handler, guarded
only by the isLoading value captured in that render. Scroll events fire
per frame, while isFetchingNextPage only becomes true after react-query
notifies and React re-renders, so several events can pass the guard for
the same page.

fetchNextPage defaults to cancelRefetch: true, so each of those calls
aborts and restarts the previous one: three calls in a frame issue three
requests and advance by a single page.

Guard on hasNextPage and isFetchingNextPage at the call site, matching
what the members table already does.
The invalidation key was built with an empty input. react-query matches
query keys partially, and an empty object matches vacuously, so every
cached searchOrganizationUsers entry was invalidated regardless of which
org it belonged to. Updating a role in one org refetched the member list
of every other org still held in cache.

Keying on the org id scopes the match to that org, while leaving `query`
unset so its filter and sort variants are still covered.
The QueryClient set only retry and refetchOnWindowFocus, leaving
staleTime at its default of 0. Combined with refetchOnMount, every mount
of every component refetched, so reference data such as roles, plans and
products was re-requested on each navigation.

Four views had worked around this locally with staleTime: Infinity, which
left the same key refetching or not depending on which page it was
reached from.

A 30s default covers navigation without holding data long enough to look
stale. Mutations invalidate their own keys and the two panels that need
immediate freshness call refetch(), which ignores staleTime, so writes are
still reflected at once. The search-backed tables keep their explicit
staleTime: 0.
Cold-loading an org from a slug URL fetched the same organization twice.
The page resolves the URL segment with getOrganization, and the view then
fetches by id: connect-query keys on the request message, so the slug and
the id are different keys and both went to the server. In-app navigation
was unaffected because it carries the id in router state and skips the
resolve, so this only hit deep links and refreshes.

Seed the id-keyed entry with the org already resolved. This is done during
render rather than in an effect: the view mounts in the same commit and
child effects run first, so an effect would seed the cache after the
request had already gone out.

Depends on a non-zero default staleTime; with staleTime 0 the seeded entry
is immediately stale and the view refetches regardless.
The details context fetched listOrganizationUsers — the full, unpaginated
member list — for every organization page, on every tab. The result was
only ever read by the projects tab: its columns render project member
avatars from it, and the add-members dropdown filters against it.

Move it behind a useOrgMembersMap hook called by those two consumers.
react-query dedupes the request between them, so the projects tab still
issues one, and the members, tokens, API, security, invoices and PAT tabs
no longer issue it at all.

The select is defined at module scope so its identity is stable and
react-query can memoize the derived map instead of rebuilding it on every
render.
The invite trigger lives in the users page navbar, so the dialog component
mounts with the page. Neither of the queries backing its fields was gated,
so searchOrganizations and listRoles ran on every visit to the users list
whether or not anyone opened the dialog.

Gate both on the dialog's open state, as the PAT details dialog already
does.
The earlier guard covered the tables rendering VirtualizedContent, whose
scroll handler fires per frame. These three only checked hasNextPage, so a
second call could still land while the previous page was in flight — and
fetchNextPage cancels the in-flight page by default, turning that into an
aborted request for no gain.

All 11 server tables now check both hasNextPage and isFetchingNextPage
before paging.
Cut each block back to the non-obvious point, and drop the load-more
comment: it was repeated verbatim in three files and the guard reads
clearly without it.
The previous guard read hasNextPage and isFetchingNextPage, but both are
last-render values and react-query notifies its observers on a macrotask.
VirtualizedContent calls onLoadMore straight from onScroll, so a burst of
scroll events clears the guard several times before React re-renders.

fetchNextPage defaults to cancelRefetch: true, so each of those extra calls
aborted the in-flight page and re-issued it — exactly the redundant requests
the guard was meant to prevent.

Add a ref that flips synchronously, so only the first call in a burst gets
through. All 11 server tables now share the shape, and the four that had the
nested conditional were inverted to the same early return.
The account id comes from listBillingAccounts, and getBillingAccount stays
disabled until it arrives — so on its own it reports "not loading" while its
consumers are still waiting. Keeping the detail tab mounted through the
billing load made that window visible for the first time: the side panel
showed N/A for the billing name, email and address, and 0 / Prepaid under
tokens, as though those were settled values rather than pending ones.

OR the two legs together in the context, and have the side-panel sections
wait on that combined flag instead of their own queries, which are disabled
for the same reason.

The same window left Edit billing and Add tokens submittable before the ids
existed, so guard both submits and disable the Add button while billing
loads. An org with no billing account still settles to false: the list
resolves empty and getBillingAccount never enables.
The slug resolve and the view's fetch are separate cache entries, and an org
edit invalidates only the id-keyed one. Seeding unconditionally meant a
remount could write the slug copy — up to its five minute staleTime old —
over fresher data, and the new default staleTime then kept it there with
nothing to trigger a correction.

Seed only when the id key holds nothing, and skip it entirely for a UUID
URL, where the resolve already owns that key and the write was a no-op that
just pushed dataUpdatedAt forward.

Also records why the resolve tolerates disabled orgs: GetOrganization
returns them to superusers only, the org-state gate answers everyone else
with FailedPrecondition, and the console is superuser-only.
Drop the notes that restated the code beneath them and cut the rest to the
one fact a reader needs in order not to undo the change.

Also corrects the QueryClient note: plans and products already set
staleTime: Infinity, so navigating never refetched them. Roles was the case
the default actually fixes.
@Shreyag02 Shreyag02 removed the Do not merge Label to indicate that the PR is not ready to be merged even though might be (or not) approvals. label Aug 17, 2026
@Shreyag02
Shreyag02 marked this pull request as ready for review August 17, 2026 16:26

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8b1080dc-9c22-45e9-bb04-474ed5412152

📥 Commits

Reviewing files that changed from the base of the PR and between 8cc1965 and 5ff0902.

📒 Files selected for processing (21)
  • web/apps/admin/src/contexts/ConnectProvider.tsx
  • web/apps/admin/src/pages/organizations/details/index.tsx
  • web/sdk/admin/hooks/useOrgMembersMap.ts
  • web/sdk/admin/views/audit-logs/index.tsx
  • web/sdk/admin/views/invoices/index.tsx
  • web/sdk/admin/views/organizations/details/apis/index.tsx
  • web/sdk/admin/views/organizations/details/contexts/organization-context.tsx
  • web/sdk/admin/views/organizations/details/edit/billing.tsx
  • web/sdk/admin/views/organizations/details/index.tsx
  • web/sdk/admin/views/organizations/details/invoices/index.tsx
  • web/sdk/admin/views/organizations/details/layout/add-tokens-dialog.tsx
  • web/sdk/admin/views/organizations/details/members/index.tsx
  • web/sdk/admin/views/organizations/details/pat/index.tsx
  • web/sdk/admin/views/organizations/details/projects/index.tsx
  • web/sdk/admin/views/organizations/details/projects/members/index.tsx
  • web/sdk/admin/views/organizations/details/side-panel/billing-details-section.tsx
  • web/sdk/admin/views/organizations/details/side-panel/tokens-details-section.tsx
  • web/sdk/admin/views/organizations/details/tokens/index.tsx
  • web/sdk/admin/views/organizations/list/index.tsx
  • web/sdk/admin/views/users/list/invite-users.tsx
  • web/sdk/admin/views/users/list/list.tsx
💤 Files with no reviewable changes (1)
  • web/sdk/admin/views/organizations/details/contexts/organization-context.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • web/sdk/admin/hooks/useOrgMembersMap.ts
  • web/apps/admin/src/contexts/ConnectProvider.tsx
  • web/apps/admin/src/pages/organizations/details/index.tsx
  • web/sdk/admin/views/organizations/details/projects/index.tsx
  • web/sdk/admin/views/organizations/details/index.tsx
  • web/sdk/admin/views/organizations/list/index.tsx

Included review availability: Your plan includes up to 2 reviews per rolling hour; 1 remains after this review.

);

const onSubmit = async (data: BillingDetailsForm) => {
if (!organizationId || !billingId) return;

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align billing controls with missing-identifier guards.

Both handlers silently no-op when required identifiers are unavailable. Keep the corresponding controls disabled, or show an explicit unavailable-account state.

  • web/sdk/admin/views/organizations/details/edit/billing.tsx#L129-L129: include missing organizationId and billingId in the Save disabled condition.
  • web/sdk/admin/views/organizations/details/layout/add-tokens-dialog.tsx#L88-L88: retain the guard for organisationId and billingAccountId.
  • web/sdk/admin/views/organizations/details/layout/add-tokens-dialog.tsx#L161-L161: include missing identifiers in the Add disabled condition.
📍 Affects 2 files
  • web/sdk/admin/views/organizations/details/edit/billing.tsx#L129-L129 (this comment)
  • web/sdk/admin/views/organizations/details/layout/add-tokens-dialog.tsx#L88-L88
  • web/sdk/admin/views/organizations/details/layout/add-tokens-dialog.tsx#L161-L161

The submit guard added earlier returns silently when the org has no billing
account, but isBillingAccountLoading settles to false in that case, so the
Add button stays enabled. The click did nothing at all: the dialog stayed
open with no toast and no console output.

Hoist the guard condition into canCheckout and feed it to the button's
disabled prop, so the state the guard rejects is no longer reachable.
Moving listOrganizationUsers out of the details provider and into
useOrgMembersMap dropped the console.error that went with it. A failed fetch
then left the projects tab rendering raw user ids in place of member avatars
with no signal anywhere, not even in the console.

Read error off the hook in the projects view and log it there, keeping the
hook a plain query wrapper and the message in line with its siblings in
details/index.tsx.
The one conflict was organizations/details/index.tsx, where both sides
reworked the same block of the provider.

- roles: take main's useOrganizationRoles hook and drop our two inline role
  queries. The hook logs both role errors itself, so removing them from the
  provider's error effect loses no logging.
- members: keep our removal. main still fetched listOrganizationUsers here;
  that query and its error log now live in useOrgMembersMap and the projects
  view, which are its only readers.
- isLoading: main's single isRolesLoading flag, still without the billing leg
  we dropped to stop the tab remounting mid-load.
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.

2 participants