feat(ui-perf): React Query scaffolding + lazy-extension rollout across 9 entity-detail pages#28017
feat(ui-perf): React Query scaffolding + lazy-extension rollout across 9 entity-detail pages#28017harshach wants to merge 7 commits into
Conversation
P3.1 of the perceived-latency design: foundational scaffolding for
incremental migration of manual fetch+Zustand patterns to React Query.
Adds the `QueryClientProvider` at the top of the app tree (outside
AuthProvider so any query made during the auth flow shares the same
cache). Defaults tuned for OpenMetadata's data shape:
- staleTime 30s — most entity reads are stable for tens of seconds
and pages flip back-and-forth
- gcTime 5min — keep results around for tab-switch round-trips
without holding memory for users who navigate away
- refetchOnWindowFocus true — picks up backend changes when the
user returns to the tab
- retry 1 — one network blip retry, no exponential backoff cascade
This is scaffolding only — no existing fetches are migrated in this
commit. Migrations land incrementally one page at a time; the next
commit is a pilot showing the pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…via useQuery
Combines a P3.3 field trim with the P3.1 React Query pilot:
P3.3 — trim: `extension` (custom property values) was eagerly requested
on every initial table-page load via `defaultFields` /
`defaultFieldsWithColumns`. Custom Properties is a single tab and the
extension blob can run into hundreds of KB on tables with many user-
defined properties. Trimming it saves wire bytes on every initial
load.
P3.1 — pilot: the lazy refetch is wired through `useQuery` with
`enabled: activeTab === EntityTabs.CUSTOM_PROPERTIES && Boolean(fqn)`.
This is the first useQuery call in the codebase — establishes the
pattern for follow-up migrations:
- Query key: stable, FQN-scoped — same key across tab toggles
- 60s staleTime: custom property values change rarely
- Auto in-flight cancellation on FQN change (free with React Query)
- Auto request dedup if the user double-clicks the tab
`tableDetails.extension` is merged in via a side-effect `useEffect` on
the query result so existing consumers (CustomPropertyTable reading
from the table state) keep working unchanged.
Files:
- utils/DatasetDetailsUtils.ts: drop EXTENSION from defaultFields and
defaultFieldsWithColumns; add new `customPropertiesFields` constant
for the lazy fetch.
- pages/TableDetailsPageV1/TableDetailsPageV1.tsx: import useQuery
+ customPropertiesFields; new useQuery hook gated on tab; effect
merges result into table state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds React Query infrastructure to the UI and pilots a performance optimization on the Table details page by trimming the eagerly requested extension field and fetching it lazily only when the Custom Properties tab is opened.
Changes:
- Add
@tanstack/react-querydependency and lockfile entries. - Introduce a shared
QueryClientand wireQueryClientProviderinto the app root. - Remove
extensionfrom default tablefields=sets and add a lazily fetchedextensionquery on the Custom Properties tab.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| openmetadata-ui/src/main/resources/ui/package.json | Adds @tanstack/react-query dependency. |
| openmetadata-ui/src/main/resources/ui/yarn.lock | Locks TanStack Query packages and updates the linked ui-core-components entry. |
| openmetadata-ui/src/main/resources/ui/src/queryClient.ts | Defines a shared app-wide QueryClient with default caching/retry behavior. |
| openmetadata-ui/src/main/resources/ui/src/App.tsx | Wraps the app in QueryClientProvider to enable React Query usage across pages. |
| openmetadata-ui/src/main/resources/ui/src/utils/DatasetDetailsUtils.ts | Removes extension from default table fields and adds a dedicated customPropertiesFields field set. |
| openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx | Adds a gated useQuery to fetch/merge extension only for the Custom Properties tab. |
| const App: FC = () => { | ||
| // QueryClientProvider sits OUTSIDE AuthProvider so any query made during the auth flow | ||
| // (e.g. fetching feature flags before login) reuses the same cache. AuthProvider remounts | ||
| // on logout — wrapping QueryClient inside would discard the cache on every logout, | ||
| // which is the opposite of what we want here. | ||
| return ( | ||
| <AuthProvider childComponentType={AppRouter}> | ||
| <AppRouter /> | ||
| </AuthProvider> | ||
| <QueryClientProvider client={queryClient}> | ||
| <AuthProvider childComponentType={AppRouter}> | ||
| <AppRouter /> | ||
| </AuthProvider> | ||
| </QueryClientProvider> |
| const { data: extensionResult } = useQuery({ | ||
| queryKey: ['table-extension', tableFqn], | ||
| queryFn: () => | ||
| getTableDetailsByFQN(tableFqn, { fields: customPropertiesFields }), | ||
| enabled: | ||
| !isTourOpen && | ||
| activeTab === EntityTabs.CUSTOM_PROPERTIES && | ||
| Boolean(tableFqn), | ||
| // Custom property values change rarely; one minute is a safe SWR window. | ||
| staleTime: 60_000, | ||
| }); |
| // Lazily fetch the `extension` field (custom properties payload) only when the user | ||
| // activates the Custom Properties tab. The eager `defaultFieldsWithColumns` deliberately | ||
| // omits `extension` because: | ||
| // - On tables with many user-defined custom properties the extension blob can be | ||
| // hundreds of KB; paying for it on every initial load is wasteful for users who never | ||
| // open Custom Properties. | ||
| // - The Custom Properties tab is the only consumer. | ||
| // Pattern used here is the P3.1 React Query pilot — `useQuery` with `enabled` gating gives | ||
| // us request dedup, in-flight cancellation on FQN change, automatic 30s SWR cache, and a | ||
| // tiny readable hook surface. Replicate this pattern for other lazy per-tab fetches. | ||
| const { data: extensionResult } = useQuery({ | ||
| queryKey: ['table-extension', tableFqn], | ||
| queryFn: () => | ||
| getTableDetailsByFQN(tableFqn, { fields: customPropertiesFields }), | ||
| enabled: | ||
| !isTourOpen && | ||
| activeTab === EntityTabs.CUSTOM_PROPERTIES && | ||
| Boolean(tableFqn), | ||
| // Custom property values change rarely; one minute is a safe SWR window. | ||
| staleTime: 60_000, | ||
| }); |
| // Fields for table details first paint. Excludes columns (paginated separately) and | ||
| // `extension` (custom properties — only the Custom Properties tab consumes this; we fetch | ||
| // it lazily when the user activates that tab via {@link customPropertiesFields}). Custom | ||
| // extension payloads can run into hundreds of KB on tables with many user-defined | ||
| // properties; trimming it saves wire bytes on every initial table-page load. | ||
| // eslint-disable-next-line max-len | ||
| export const defaultFields = `${TabSpecificField.FOLLOWERS},${TabSpecificField.JOINS},${TabSpecificField.TAGS},${TabSpecificField.OWNERS},${TabSpecificField.DATAMODEL},${TabSpecificField.TABLE_CONSTRAINTS},${TabSpecificField.SCHEMA_DEFINITION},${TabSpecificField.DOMAINS},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.VOTES},${TabSpecificField.EXTENSION}`; | ||
| export const defaultFields = `${TabSpecificField.FOLLOWERS},${TabSpecificField.JOINS},${TabSpecificField.TAGS},${TabSpecificField.OWNERS},${TabSpecificField.DATAMODEL},${TabSpecificField.TABLE_CONSTRAINTS},${TabSpecificField.SCHEMA_DEFINITION},${TabSpecificField.DOMAINS},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.VOTES}`; | ||
|
|
||
| // Legacy fields that include columns - only use when pagination is not needed | ||
| // eslint-disable-next-line max-len | ||
| export const defaultFieldsWithColumns = `${TabSpecificField.COLUMNS},${TabSpecificField.FOLLOWERS},${TabSpecificField.JOINS},${TabSpecificField.TAGS},${TabSpecificField.OWNERS},${TabSpecificField.DATAMODEL},${TabSpecificField.TABLE_CONSTRAINTS},${TabSpecificField.SCHEMA_DEFINITION},${TabSpecificField.DOMAINS},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.VOTES},${TabSpecificField.EXTENSION}`; | ||
| export const defaultFieldsWithColumns = `${TabSpecificField.COLUMNS},${TabSpecificField.FOLLOWERS},${TabSpecificField.JOINS},${TabSpecificField.TAGS},${TabSpecificField.OWNERS},${TabSpecificField.DATAMODEL},${TabSpecificField.TABLE_CONSTRAINTS},${TabSpecificField.SCHEMA_DEFINITION},${TabSpecificField.DOMAINS},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.VOTES}`; | ||
|
|
||
| // Lazy field set requested only when the Custom Properties tab is activated. Pairs with | ||
| // the trim of {@link defaultFields} above. | ||
| export const customPropertiesFields = `${TabSpecificField.EXTENSION}`; |
| setTableDetails((prev) => | ||
| prev ? { ...prev, extension: extensionResult.extension } : prev | ||
| ); |
| // Pattern used here is the P3.1 React Query pilot — `useQuery` with `enabled` gating gives | ||
| // us request dedup, in-flight cancellation on FQN change, automatic 30s SWR cache, and a | ||
| // tiny readable hook surface. Replicate this pattern for other lazy per-tab fetches. | ||
| const { data: extensionResult } = useQuery({ | ||
| queryKey: ['table-extension', tableFqn], | ||
| queryFn: () => | ||
| getTableDetailsByFQN(tableFqn, { fields: customPropertiesFields }), | ||
| enabled: | ||
| !isTourOpen && | ||
| activeTab === EntityTabs.CUSTOM_PROPERTIES && | ||
| Boolean(tableFqn), | ||
| // Custom property values change rarely; one minute is a safe SWR window. | ||
| staleTime: 60_000, |
…a useQuery
Finishes the P3.3 audit started in the previous commit and applies the
P3.1 React Query pattern across the catalogue of entity-detail pages.
P3.3 — `extension` (custom-property values) trimmed from `defaultFields`
in 9 utils files. The blob can run into hundreds of KB on entities with
many user-defined custom properties; only the Custom Properties tab
consumes it. Trimming saves wire bytes on every initial page load.
- utils/DatasetDetailsUtils.ts (Table — already trimmed; cleanup)
- utils/DashboardDetailsUtils.tsx (Dashboard)
- utils/PipelineDetailsUtils.tsx (Pipeline)
- utils/MlModelDetailsUtils.tsx (MlModel)
- utils/StoredProceduresUtils.tsx (StoredProcedure)
- utils/SearchIndexUtils.tsx (SearchIndex)
- utils/DirectoryDetailsUtils.tsx (Directory)
- utils/SpreadsheetDetailsUtils.tsx (Spreadsheet)
- utils/WorksheetDetailsUtils.tsx (Worksheet)
P3.1 — extracted the lazy-fetch pattern into a reusable hook so each
page's wiring is 4 lines instead of a copy-pasted useQuery + useEffect:
hooks/useLazyEntityExtension.ts — generic over entity shape, takes
(entityType, fqn, activeTab, fetcher, onResolve). Internally:
- useQuery gated on `activeTab === CUSTOM_PROPERTIES && fqn`
- 60s staleTime — custom property values change rarely
- Hardcoded `TabSpecificField.EXTENSION` field — single canonical
enum, removes per-page constants
- onResolve callback shape (rather than passing setState directly)
so each consumer handles their own state-shape semantics — some
pages init state as `{} as T`, others as `useState<T>()`.
Hook integrated on 6 pages — Table (refactored from inline pilot to
use the hook), Dashboard, Pipeline, MlModel, SearchIndex, StoredProcedure.
Drive entity pages (Directory, Spreadsheet, Worksheet) have their utils
trimmed but the page-level hook integration is left as follow-up; their
fetcher (`getDriveAssetByFqn<T>`) is generic and needs slightly different
wiring per page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the lazy-extension rollout to Directory, Spreadsheet, and Worksheet pages — paired with the EXTENSION trim already applied to their utils files. Drive pages share `getDriveAssetByFqn<T>(fqn, entityType, fields)` which has a different signature than other entity fetchers (entityType is a positional argument, not bundled in `params`). Each page wraps the call in a small adapter closure to match `useLazyEntityExtension`'s expected fetcher shape `(fqn, params) => Promise<T>`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
P3.1 worked-template: the main `getTableDetailsByFQN` fetch on
TableDetailsPageV1 — by far the highest-traffic entity-detail page in
OpenMetadata — moves from a hand-rolled `useState + useCallback +
useEffect` pattern to React Query.
Why TableDetailsPageV1 first: it's the heaviest page (~25 setTableDetails
mutation call sites, tour-mode override, permission-gated fetch, post-
edit refetch on vote). Migrating it first establishes the precise
recipe other entity-detail pages can follow systematically.
What changed:
- `useState<Table>()` + `useState(loading)` + `fetchTableDetails` callback
replaced by a single `useQuery({ queryKey, queryFn, enabled })`.
- Stable queryKey of `['table-detail', tableFqn, fieldsString]` —
permission changes mutate the fields string and invalidate the cache
automatically; FQN changes swap cache slot or refetch.
- Permissions gating: `enabled` waits for `tablePermissionsLoaded`
(sentinel-check on `DEFAULT_ENTITY_PERMISSION` reference) so the
query doesn't race the permission fetch.
- Tour-mode mock data injected via `queryClient.setQueryData(key, mock)`
— useQuery picks it up because `data` is sourced from the cache.
- FORBIDDEN navigation moved from try/catch into a useEffect on
`tableQueryError`; addToRecentViewed moved into a useEffect on
`tableDetails?.id`.
- `loading` derives `isTableLoading || (permissions still loading)` so
the page doesn't briefly render the no-data placeholder before the
query is even enabled.
Backward-compat shim — preserves the call-site contract:
- `setTableDetails(value)` and `setTableDetails(updater)` continue to
work via a wrapper that forwards to `queryClient.setQueryData`. The
~25 mutation call sites in this file (edit handlers, follow,
unfollow, vote, restore, certification, tier, suggestions) need NO
changes — they keep writing to "tableDetails" and reads stay
consistent because both sides are now backed by the cache.
- `fetchTableDetails()` becomes a thin wrapper around `refetch()`.
Reorder: `extraDropdownContent` useMemo moved to AFTER the useQuery
block because it now reads `tableDetails` from the query (which must
be defined first). No behaviour change.
Side-effects from the legacy FQN-change useEffect now live in two
focused effects: tour-mode cache priming, and getEntityFeedCount.
Verified: yarn build green, eslint clean, tsc clean (the one
pre-existing tsc error in this file is unrelated — `findColumnByEntityLink`
on a `string | undefined`).
Other entity-detail pages (Dashboard, Pipeline, MlModel, etc.) follow
the same recipe in follow-up commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 23 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
openmetadata-ui/src/main/resources/ui/src/utils/WorksheetDetailsUtils.tsx:49
defaultFieldsincludesTabSpecificField.ROW_COUNTtwice, which results in a duplicated field in thefields=query param. This is unnecessary work and can make debugging field selection harder; please remove the duplicate (or dedupe the list before joining).
export const defaultFields = [
TabSpecificField.OWNERS,
TabSpecificField.FOLLOWERS,
TabSpecificField.TAGS,
TabSpecificField.DOMAINS,
TabSpecificField.DATA_PRODUCTS,
TabSpecificField.VOTES,
TabSpecificField.ROW_COUNT,
TabSpecificField.COLUMNS,
TabSpecificField.ROW_COUNT,
].join(',');
| } catch { | ||
| showErrorToast( | ||
| t('server.fetch-entity-permissions-error', { | ||
| entity: t('label.resource-permission-lowercase'), | ||
| }) |
| // QueryClientProvider sits OUTSIDE AuthProvider so any query made during the auth flow | ||
| // (e.g. fetching feature flags before login) reuses the same cache. AuthProvider remounts | ||
| // on logout — wrapping QueryClient inside would discard the cache on every logout, | ||
| // which is the opposite of what we want here. | ||
| return ( | ||
| <AuthProvider childComponentType={AppRouter}> | ||
| <AppRouter /> | ||
| </AuthProvider> | ||
| <QueryClientProvider client={queryClient}> | ||
| <AuthProvider childComponentType={AppRouter}> | ||
| <AppRouter /> | ||
| </AuthProvider> | ||
| </QueryClientProvider> |
| // Main entity fetch — migrated from a hand-rolled `useState + useCallback + useEffect` | ||
| // pattern to React Query (P3.1). Replaces `fetchTableDetails` and the `[tableDetails, | ||
| // setTableDetails]` useState below. Existing call sites that did `setTableDetails(...)` or | ||
| // `fetchTableDetails()` continue to work via the wrapper functions defined below — the | ||
| // page state-shape contract is preserved. | ||
| const { |
🔴 Playwright Results — 23 failure(s), 18 flaky✅ 4015 passed · ❌ 23 failed · 🟡 18 flaky · ⏭️ 151 skipped
Genuine Failures (failed on all attempts)❌
|
… tests Addresses open review comments on PR #28017: * **Security — clear QueryClient cache on logout** (AuthProvider.tsx). Before: `QueryClientProvider` sits outside `AuthProvider`, so cached query data survives logout. On a shared machine, the next user could briefly see the previous user's cached entity payloads. Add `queryClient.clear()` to `onLogoutHandler` so the cache is wiped at the same moment auth state is cleared. * **Bug — page no longer stuck in loading state if permission fetch fails** (TableDetailsPageV1.tsx). Before: `tablePermissionsLoaded` was derived from `tablePermissions !== DEFAULT_ENTITY_PERMISSION`. If `fetchResourcePermission` threw, permissions stayed as the sentinel and the page loader never resolved. Now explicit `tablePermissionsLoaded` state, flipped to `true` in a `finally` block so the page progresses even on permission error. * **Bug — gate `useQuery` on `viewBasicPermission`** so users without view permission see the permission-error placeholder instead of triggering a guaranteed-403 fetch. Matches the legacy `if (viewBasicPermission) fetchTableDetails()` gating. `viewBasicPermission` hoisted into the upstream `useMemo` so it is available to the query's `enabled` clause. * **Tests — TableDetailsPageV1 unit suite migrated for React Query**: add a `renderWithProviders` helper that wraps with a fresh `QueryClientProvider`, remove `extension` from the `COMMON_API_FIELDS` constant (matches the trimmed default-fields), add `addToRecentViewed` to the `CommonUtils` mock (now driven by an effect on resolved entity id), and switch the three sync `getByText` assertions that race the query resolve to `findByText`. * **Tests — global `useLazyEntityExtension` mock in `setupTests.js`** so page-component tests that render an entity-detail page do not need to set up React Query context just to render. Per-test overrides remain possible. * **Tests — update `fields=` assertions** on Dashboard/SearchIndex/Spreadsheet/Worksheet test files to reflect the trimmed default-fields (no `extension`). * **UI checkstyle — sort imports** on the 6 page files flagged by the CI lint-src job. No functional change. Verified locally: 47/47 tests pass across TableDetailsPageV1/Dashboard/Pipeline/MlModel/SearchIndex/StoredProcedure/AuthProvider. Pre-existing test failures on Directory/Spreadsheet/Worksheet (useParams mock not wired) are unaffected by this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review ✅ Approved 3 resolved / 3 findingsIntroduces React Query infrastructure and optimizes load times by lazy-loading the ✅ 3 resolved✅ Edge Case: useEffect merge overwrites extension updated via Custom Properties tab
✅ Security: QueryClient cache not cleared on logout leaks data between users
✅ Bug: Page stuck in infinite loading state if permission fetch fails
OptionsDisplay: compact → Showing less information. Comment with these commands to change:
Was this helpful? React with 👍 / 👎 | Gitar |
Brings the unique work from PR #28017 into this branch so we can close that PR as superseded. Trims `extension` (custom-property values) from `defaultFields` on 9 entity-detail pages — Table, Dashboard, Pipeline, MlModel, StoredProcedure, SearchIndex, Directory, Spreadsheet, Worksheet — and fetches it lazily on Custom Properties tab activation via a new `useLazyEntityExtension` hook. Why this matters: - Custom property payloads can run into hundreds of KB on entities with many user-defined properties. Most users never open the Custom Properties tab, so paying for it on first paint is wasted bytes. - The hook centralises the gated-useQuery + merge-into-state pattern (60s staleTime, FQN-scoped queryKey, auto cancellation on FQN change) so each page's wiring is 4–8 lines instead of a copy-pasted closure-with-effect. Test plumbing: - `setupTests.js` globally mocks `useLazyEntityExtension` to a no-op so page tests that render without `QueryClientProvider` keep rendering. - Per-page `fields=` assertions updated where they hardcode the trimmed default-fields string. - Drive entity tests gain a `useParams` mock (returns `{ tab: ... }`) so the page's `useRequiredParams` doesn't throw during render. Closes the lazy-extension work from PR #28017; that PR can now be closed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Superseded by PR #28014 (perceived-latency-p1). The unique work from this PR — the |
Describe your changes:
Completes P3.3 (per-page
fields=trim) and P3.1 (React Query foundation) on the perceived-latency design doc. Goes beyond the initial pilot — the lazy-extension pattern is now applied across all 9 entity-detail pages that requestedEXTENSIONeagerly, and a reusable hook captures the pattern for follow-up migrations.P3.1 — React Query infrastructure
@tanstack/react-querydep added (codebase had zero query libraries before).src/queryClient.ts— single sharedQueryClientwith sensible defaults:staleTime: 30s,gcTime: 5min,refetchOnWindowFocus: true,retry: 1.App.tsxwrapsAuthProvider + AppRouterinQueryClientProvider(outsideAuthProviderso the cache survives logout/login transitions).P3.1 — Reusable lazy-fetch hook
src/hooks/useLazyEntityExtension.ts— generic over entity shape, takes(entityType, fqn, activeTab, fetcher, onResolve). Internally:useQuerygated onactiveTab === EntityTabs.CUSTOM_PROPERTIES && Boolean(fqn)staleTime— custom-property values change rarelyTabSpecificField.EXTENSIONfield (canonical enum, no per-page constants)onResolve(rather than passingsetState) — different pages init state as{} as TvsuseState<T>(); the callback shape lets each consumer handle their own state semanticsP3.3 —
EXTENSIONtrim across 9 entity-detail pagesThe custom-property values blob can run into hundreds of KB on entities with many user-defined properties; only the Custom Properties tab consumes it. Trimmed eagerly-requested
EXTENSIONfromdefaultFieldsin:utils/DatasetDetailsUtils.ts(Table)utils/DashboardDetailsUtils.tsx(Dashboard)utils/PipelineDetailsUtils.tsx(Pipeline)utils/MlModelDetailsUtils.tsx(MlModel)utils/StoredProceduresUtils.tsx(StoredProcedure)utils/SearchIndexUtils.tsx(SearchIndex)utils/DirectoryDetailsUtils.tsx(Directory)utils/SpreadsheetDetailsUtils.tsx(Spreadsheet)utils/WorksheetDetailsUtils.tsx(Worksheet)P3.1 — Hook applied across 9 entity-detail pages
Lazy
useLazyEntityExtensioncall wired on:pages/TableDetailsPageV1/TableDetailsPageV1.tsx(refactored from inline pilot to use the hook)pages/DashboardDetailsPage/DashboardDetailsPage.component.tsxpages/PipelineDetails/PipelineDetailsPage.component.tsxpages/MlModelPage/MlModelPage.component.tsxpages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsxpages/StoredProcedure/StoredProcedurePage.tsxpages/DirectoryDetailsPage/DirectoryDetailsPage.tsx(drive — adaptsgetDriveAssetByFqn<T>signature via closure)pages/SpreadsheetDetailsPage/SpreadsheetDetailsPage.tsx(drive — adapter)pages/WorksheetDetailsPage/WorksheetDetailsPage.tsx(drive — adapter)Each page activation of Custom Properties tab now fires a single targeted
GET ?fields=extensioninstead of paying for the field on every page load.What's NOT in this PR (honest scope statement)
getXyzByFqnfetch to useQuery — this is the big-picture P3.1 work, but it's a multi-PR refactor. Each page has 10-20setXxxDetails(...)call sites in edit handlers, follow handlers, vote handlers, etc. that would need to be converted toqueryClient.setQueryData(...)for cache consistency. Mistakes there cause stale UI bugs in production. This PR ships the foundation + the safe lazy-extension migration; the main-fetch migrations land incrementally one page at a time.votes,followerson tabs they're not visible from) — separate audit per design doc.Verification
yarn buildsucceeds; the previous AsyncDeleteProvider grab-bag chunk is unchanged in size (this PR's wins are at runtime, not build-time).npx tsc --noEmitclean on all 18 touched files (3 pre-existing unrelatedlodash.gettyping issues in Drive pagefollowXhandlers — not introduced by this PR).extension→ click Custom Properties tab → confirm a separateGET ?fields=extensionfires → click Schema then Custom Properties again within 60s → confirm cache hit (no network).Type of change:
Frontend Preview (Loom)
N/A — no visual change. Verification path above.
Checklist:
<type>: <title>and follows Conventional Commits Specificationyarn buildsucceeds.🤖 Generated with Claude Code
Summary by Gitar
TableDetailsPageV1primary entity fetch touseQuery, preserving state-contract viasetTableDetailsandfetchTableDetailswrappers.isTourOpenstatus.This will update automatically on new commits.