From bfb0005756d67384f08b04b280b50fb059685dde Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Sun, 10 May 2026 10:33:47 -0700 Subject: [PATCH 1/6] feat(ui-perf): add @tanstack/react-query + QueryClientProvider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../src/main/resources/ui/package.json | 1 + .../src/main/resources/ui/src/App.tsx | 14 +++++-- .../src/main/resources/ui/src/queryClient.ts | 42 +++++++++++++++++++ .../src/main/resources/ui/yarn.lock | 17 ++++++-- 4 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/queryClient.ts diff --git a/openmetadata-ui/src/main/resources/ui/package.json b/openmetadata-ui/src/main/resources/ui/package.json index 494d4a905f67..5d77de2cecb6 100644 --- a/openmetadata-ui/src/main/resources/ui/package.json +++ b/openmetadata-ui/src/main/resources/ui/package.json @@ -85,6 +85,7 @@ "@rjsf/core": "5.24.13", "@rjsf/utils": "5.24.13", "@rjsf/validator-ajv8": "5.24.13", + "@tanstack/react-query": "^5.62.0", "@tiptap/core": "^2.3.0", "@tiptap/extension-link": "^2.10.4", "@tiptap/extension-placeholder": "^2.3.0", diff --git a/openmetadata-ui/src/main/resources/ui/src/App.tsx b/openmetadata-ui/src/main/resources/ui/src/App.tsx index 35f6d39c8ce1..66cc89e3b862 100644 --- a/openmetadata-ui/src/main/resources/ui/src/App.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/App.tsx @@ -11,15 +11,23 @@ * limitations under the License. */ +import { QueryClientProvider } from '@tanstack/react-query'; import { FC } from 'react'; import AppRouter from './components/AppRouter/AppRouter'; import { AuthProvider } from './components/Auth/AuthProviders/AuthProvider'; +import { queryClient } from './queryClient'; 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 ( - - - + + + + + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/queryClient.ts b/openmetadata-ui/src/main/resources/ui/src/queryClient.ts new file mode 100644 index 000000000000..371ea4ff476b --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/queryClient.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { QueryClient } from '@tanstack/react-query'; + +/** + * App-wide React Query client. + * + * Defaults are tuned for OpenMetadata's data shape: + * - {@link staleTime} 30 s — most entity reads are stable for tens of seconds at a time + * and pages flip back-and-forth (Schema → Lineage → Schema). The same query reissued + * within the window serves cached data instead of refetching. + * - {@link gcTime} 5 min — keep results around long enough for a tab-switch round-trip + * without holding memory for users who navigate away. + * - {@link refetchOnWindowFocus} true — picks up backend changes when the user returns + * to the tab, but not so aggressively that idle tabs hammer the API. + * - {@link retry} 1 — one network blip retry, no exponential backoff cascade. + * + * Per-query overrides in `useQuery({ queryKey, queryFn, staleTime, ... })` always win. + */ +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + gcTime: 5 * 60_000, + refetchOnWindowFocus: true, + retry: 1, + }, + mutations: { + retry: 0, + }, + }, +}); diff --git a/openmetadata-ui/src/main/resources/ui/yarn.lock b/openmetadata-ui/src/main/resources/ui/yarn.lock index 60466dff516a..6533a6a7d67c 100644 --- a/openmetadata-ui/src/main/resources/ui/yarn.lock +++ b/openmetadata-ui/src/main/resources/ui/yarn.lock @@ -2375,9 +2375,8 @@ compare-versions "^4.1.2" "@openmetadata/ui-core-components@link:../../../../../openmetadata-ui-core-components/src/main/resources/ui": - version "1.0.0" - dependencies: - "@material/material-color-utilities" "^0.3.0" + version "0.0.0" + uid "" "@peculiar/asn1-schema@^2.3.13", "@peculiar/asn1-schema@^2.3.8": version "2.6.0" @@ -3542,6 +3541,18 @@ "@tailwindcss/oxide" "4.2.4" tailwindcss "4.2.4" +"@tanstack/query-core@5.100.9": + version "5.100.9" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.100.9.tgz#dcf44ef25cf42a4da229bcab1d8d33e80a740a99" + integrity sha512-SJSFw1S8+kQ0+knv/XGfrbocWoAlT7vDKsSImtLx3ZPQmEcR46hkDjLSvynSy25N8Ms4tIEini1FuBd5k7IscQ== + +"@tanstack/react-query@^5.62.0": + version "5.100.9" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.100.9.tgz#0c701bf56f38b484602255a92d4c9e452a04807d" + integrity sha512-Oa44XkaI3kCNN6ME0KByU3xT3SEUNOMfZpHxL6+wFoTm+OeUFYHKdeYVe0aOXlRDm/f15sgLwEt2HDorIdW8+A== + dependencies: + "@tanstack/query-core" "5.100.9" + "@testing-library/dom@^9.0.0": version "9.3.4" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.4.tgz#50696ec28376926fec0a1bf87d9dbac5e27f60ce" From c23930ab040eedbe0472cb59d793648e4fa6201c Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Sun, 10 May 2026 10:34:01 -0700 Subject: [PATCH 2/6] perf(ui-perf): trim `extension` from table first-paint, fetch lazily via useQuery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../TableDetailsPageV1/TableDetailsPageV1.tsx | 33 +++++++++++++++++++ .../ui/src/utils/DatasetDetailsUtils.ts | 14 ++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index 966e5ead5f89..78ba57c7bf60 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -11,6 +11,7 @@ * limitations under the License. */ +import { useQuery } from '@tanstack/react-query'; import { Col, Row, Tabs, Tooltip } from 'antd'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; @@ -78,6 +79,7 @@ import { getTabLabelMapFromTabs, } from '../../utils/CustomizePage/CustomizePageUtils'; import { + customPropertiesFields, defaultFields, defaultFieldsWithColumns, } from '../../utils/DatasetDetailsUtils'; @@ -233,6 +235,37 @@ const TableDetailsPageV1: React.FC = () => { [tableFqn, viewUsagePermission] ); + // 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, + }); + + useEffect(() => { + if (!extensionResult) { + return; + } + setTableDetails((prev) => + prev ? { ...prev, extension: extensionResult.extension } : prev + ); + }, [extensionResult]); + const fetchDQUpstreamFailureCount = async () => { if (!tableClassBase.getAlertEnableStatus()) { setDqFailureCount(0); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatasetDetailsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DatasetDetailsUtils.ts index c04f9f1152df..693a7a873adc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatasetDetailsUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatasetDetailsUtils.ts @@ -13,12 +13,20 @@ import { TabSpecificField } from '../enums/entity.enum'; -// Fields for table details - excludes columns which will be fetched separately with pagination +// 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}`; export const commonTableFields = `${TabSpecificField.TAGS},${TabSpecificField.OWNERS},${TabSpecificField.DOMAINS},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.CERTIFICATION}`; From a636e99980fe69a698c1de5cd7142b57f1bcf868 Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Sun, 10 May 2026 10:50:35 -0700 Subject: [PATCH 3/6] perf(ui-perf): trim EXTENSION across all entity-detail pages, lazy via useQuery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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()`. 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`) is generic and needs slightly different wiring per page. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ui/src/hooks/useLazyEntityExtension.ts | 92 +++++++++++++++++++ .../DashboardDetailsPage.component.tsx | 19 +++- .../MlModelPage/MlModelPage.component.tsx | 19 +++- .../PipelineDetailsPage.component.tsx | 19 +++- .../SearchIndexDetailsPage.tsx | 11 +++ .../StoredProcedure/StoredProcedurePage.tsx | 11 +++ .../TableDetailsPageV1/TableDetailsPageV1.tsx | 43 +++------ .../ui/src/utils/DashboardDetailsUtils.tsx | 5 +- .../ui/src/utils/DatasetDetailsUtils.ts | 8 +- .../ui/src/utils/DirectoryDetailsUtils.tsx | 3 +- .../ui/src/utils/MlModelDetailsUtils.tsx | 4 +- .../ui/src/utils/PipelineDetailsUtils.tsx | 5 +- .../ui/src/utils/SearchIndexUtils.tsx | 4 +- .../ui/src/utils/SpreadsheetDetailsUtils.tsx | 3 +- .../ui/src/utils/StoredProceduresUtils.tsx | 4 +- .../ui/src/utils/WorksheetDetailsUtils.tsx | 3 +- 16 files changed, 205 insertions(+), 48 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/hooks/useLazyEntityExtension.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useLazyEntityExtension.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useLazyEntityExtension.ts new file mode 100644 index 000000000000..c479377a00df --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useLazyEntityExtension.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2026 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useQuery } from '@tanstack/react-query'; +import { useEffect } from 'react'; +import { EntityTabs, TabSpecificField } from '../enums/entity.enum'; + +interface EntityWithExtension { + extension?: unknown; +} + +/** + * Lazily fetch an entity's `extension` (custom-property values) only when the user activates + * the Custom Properties tab. Replaces what used to be eager inclusion of {@code EXTENSION} + * in {@code defaultFields} on every entity-detail page load. + * + * Why this exists: + * - 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 pattern (gated useQuery + merge into local state) was the same on every entity + * detail page; centralising it avoids 8 copies of the same closure-with-effect. + * + * Per-page wiring (call at the page top-level, alongside the main entity state): + *
+ *   useLazyEntityExtension<Dashboard>({
+ *     entityType: EntityType.DASHBOARD,
+ *     fqn: dashboardFQN,
+ *     activeTab,
+ *     fetcher: getDashboardByFqn,
+ *     onResolve: (extension) =>
+ *       setDashboardDetails((prev) => ({ ...prev, extension })),
+ *   });
+ * 
+ * + * The {@code onResolve} callback shape (rather than passing a setState directly) keeps each + * consumer in control of their own state-shape semantics — some pages init state as + * `{} as T` (non-undefined), others as `useState()` (T | undefined). Either works. + * + * Behaviour: + * - Query is gated by `enabled: activeTab === CUSTOM_PROPERTIES && Boolean(fqn)` — does + * nothing on other tabs. + * - Stable `queryKey` of `[`-extension`, fqn]` — cached across tab toggles, refetched + * on FQN change with automatic in-flight cancellation. + * - 60s {@code staleTime} — custom property values change rarely. + * - On resolve, fires {@code onResolve(extension)} exactly once per fresh fetch. + * + * Caveats: + * - The {@code onResolve} callback identity is not memoised at the call site. We + * deliberately depend only on `data?.extension` so we don't fire the merge effect on + * every parent re-render — the latest callback is captured at fire time. + */ +export function useLazyEntityExtension({ + entityType, + fqn, + activeTab, + fetcher, + onResolve, +}: { + entityType: string; + fqn: string | undefined; + activeTab: string | undefined; + fetcher: (fqn: string, params: { fields: string }) => Promise; + onResolve: (extension: T['extension']) => void; +}): void { + const enabled = activeTab === EntityTabs.CUSTOM_PROPERTIES && Boolean(fqn); + + const { data } = useQuery({ + queryKey: [`${entityType}-extension`, fqn], + queryFn: () => + fetcher(fqn as string, { fields: TabSpecificField.EXTENSION }), + enabled, + staleTime: 60_000, + }); + + useEffect(() => { + if (data?.extension === undefined) { + return; + } + onResolve(data.extension); + // onResolve is intentionally omitted from deps — see header comment. + }, [data?.extension]); +} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx index 9e991915ae19..42629e9dcc90 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx @@ -27,12 +27,18 @@ import { usePermissionProvider } from '../../context/PermissionProvider/Permissi import { ResourceEntity } from '../../context/PermissionProvider/PermissionProvider.interface'; import { ClientErrors } from '../../enums/Axios.enum'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; -import { EntityType, TabSpecificField } from '../../enums/entity.enum'; +import { + EntityTabs, + EntityType, + TabSpecificField, +} from '../../enums/entity.enum'; import { Chart } from '../../generated/entity/data/chart'; import { Dashboard } from '../../generated/entity/data/dashboard'; import { Operation as PermissionOperation } from '../../generated/entity/policies/accessControl/resourcePermission'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; +import { useLazyEntityExtension } from '../../hooks/useLazyEntityExtension'; +import { useRequiredParams } from '../../utils/useRequiredParams'; import { addFollower, getDashboardByFqn, @@ -64,10 +70,21 @@ const DashboardDetailsPage = () => { const navigate = useNavigate(); const { getEntityPermissionByFqn } = usePermissionProvider(); const { entityFqn: dashboardFQN } = useFqn({ type: EntityType.DASHBOARD }); + const { tab: activeTab } = useRequiredParams<{ tab: EntityTabs }>(); const [dashboardDetails, setDashboardDetails] = useState( {} as Dashboard ); + + // Lazy custom-properties fetch — see {@link useLazyEntityExtension}. + useLazyEntityExtension({ + entityType: EntityType.DASHBOARD, + fqn: dashboardFQN, + activeTab, + fetcher: getDashboardByFqn, + onResolve: (extension) => + setDashboardDetails((prev) => ({ ...prev, extension })), + }); const [isLoading, setLoading] = useState(false); const [isError, setIsError] = useState(false); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx index 1e1c57d35d0c..81b9a296beee 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx @@ -27,11 +27,17 @@ import { usePermissionProvider } from '../../context/PermissionProvider/Permissi import { ResourceEntity } from '../../context/PermissionProvider/PermissionProvider.interface'; import { ClientErrors } from '../../enums/Axios.enum'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; -import { EntityType, TabSpecificField } from '../../enums/entity.enum'; +import { + EntityTabs, + EntityType, + TabSpecificField, +} from '../../enums/entity.enum'; import { Mlmodel } from '../../generated/entity/data/mlmodel'; import { Operation as PermissionOperation } from '../../generated/entity/policies/accessControl/resourcePermission'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; +import { useLazyEntityExtension } from '../../hooks/useLazyEntityExtension'; +import { useRequiredParams } from '../../utils/useRequiredParams'; import { addFollower, getMlModelByFQN, @@ -57,7 +63,18 @@ const MlModelPage = () => { const { currentUser } = useApplicationStore(); const navigate = useNavigate(); const { entityFqn: mlModelFqn } = useFqn({ type: EntityType.MLMODEL }); + const { tab: activeTab } = useRequiredParams<{ tab: EntityTabs }>(); const [mlModelDetail, setMlModelDetail] = useState({} as Mlmodel); + + // Lazy custom-properties fetch — see {@link useLazyEntityExtension}. + useLazyEntityExtension({ + entityType: EntityType.MLMODEL, + fqn: mlModelFqn, + activeTab, + fetcher: getMlModelByFQN, + onResolve: (extension) => + setMlModelDetail((prev) => ({ ...prev, extension })), + }); const [isDetailLoading, setIsDetailLoading] = useState(false); const USERId = currentUser?.id ?? ''; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/PipelineDetails/PipelineDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/PipelineDetails/PipelineDetailsPage.component.tsx index 7cd312470555..4f1befd9c226 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/PipelineDetails/PipelineDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/PipelineDetails/PipelineDetailsPage.component.tsx @@ -27,12 +27,18 @@ import { usePermissionProvider } from '../../context/PermissionProvider/Permissi import { ResourceEntity } from '../../context/PermissionProvider/PermissionProvider.interface'; import { ClientErrors } from '../../enums/Axios.enum'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; -import { EntityType, TabSpecificField } from '../../enums/entity.enum'; +import { + EntityTabs, + EntityType, + TabSpecificField, +} from '../../enums/entity.enum'; import { Pipeline } from '../../generated/entity/data/pipeline'; import { Operation as PermissionOperation } from '../../generated/entity/policies/accessControl/resourcePermission'; import { Paging } from '../../generated/type/paging'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; +import { useLazyEntityExtension } from '../../hooks/useLazyEntityExtension'; +import { useRequiredParams } from '../../utils/useRequiredParams'; import { addFollower, getPipelineByFqn, @@ -62,10 +68,21 @@ const PipelineDetailsPage = () => { const { entityFqn: decodedPipelineFQN } = useFqn({ type: EntityType.PIPELINE, }); + const { tab: activeTab } = useRequiredParams<{ tab: EntityTabs }>(); const [pipelineDetails, setPipelineDetails] = useState( {} as Pipeline ); + // Lazy custom-properties fetch — see {@link useLazyEntityExtension}. + useLazyEntityExtension({ + entityType: EntityType.PIPELINE, + fqn: decodedPipelineFQN, + activeTab, + fetcher: getPipelineByFqn, + onResolve: (extension) => + setPipelineDetails((prev) => ({ ...prev, extension })), + }); + const [isLoading, setLoading] = useState(true); const [isError, setIsError] = useState(false); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx index d1da2ef2a6b8..e63c685dc1c7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.tsx @@ -45,6 +45,7 @@ import LimitWrapper from '../../hoc/LimitWrapper'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useCustomPages } from '../../hooks/useCustomPages'; import { useFqn } from '../../hooks/useFqn'; +import { useLazyEntityExtension } from '../../hooks/useLazyEntityExtension'; import { FeedCounts } from '../../interface/feed.interface'; import { addFollower, @@ -89,6 +90,16 @@ function SearchIndexDetailsPage() { const USERId = currentUser?.id ?? ''; const [loading, setLoading] = useState(true); const [searchIndexDetails, setSearchIndexDetails] = useState(); + + // Lazy custom-properties fetch — see {@link useLazyEntityExtension}. + useLazyEntityExtension({ + entityType: EntityType.SEARCH_INDEX, + fqn: decodedSearchIndexFQN, + activeTab, + fetcher: getSearchIndexDetailsByFQN, + onResolve: (extension) => + setSearchIndexDetails((prev) => (prev ? { ...prev, extension } : prev)), + }); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx index 1d3bbaed362a..2cfde5f325ff 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/StoredProcedure/StoredProcedurePage.tsx @@ -48,6 +48,7 @@ import LimitWrapper from '../../hoc/LimitWrapper'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useCustomPages } from '../../hooks/useCustomPages'; import { useFqn } from '../../hooks/useFqn'; +import { useLazyEntityExtension } from '../../hooks/useLazyEntityExtension'; import { FeedCounts } from '../../interface/feed.interface'; import { addStoredProceduresFollower, @@ -93,6 +94,16 @@ const StoredProcedurePage = () => { const { getEntityPermissionByFqn } = usePermissionProvider(); const [isLoading, setIsLoading] = useState(true); const [storedProcedure, setStoredProcedure] = useState(); + + // Lazy custom-properties fetch — see {@link useLazyEntityExtension}. + useLazyEntityExtension({ + entityType: EntityType.STORED_PROCEDURE, + fqn: decodedStoredProcedureFQN, + activeTab, + fetcher: getStoredProceduresByFqn, + onResolve: (extension) => + setStoredProcedure((prev) => (prev ? { ...prev, extension } : prev)), + }); const [storedProcedurePermissions, setStoredProcedurePermissions] = useState(DEFAULT_ENTITY_PERMISSION); const [isTabExpanded, setIsTabExpanded] = useState(false); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index 78ba57c7bf60..43fa12aa630a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -11,7 +11,6 @@ * limitations under the License. */ -import { useQuery } from '@tanstack/react-query'; import { Col, Row, Tabs, Tooltip } from 'antd'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; @@ -58,6 +57,7 @@ import LimitWrapper from '../../hoc/LimitWrapper'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useCustomPages } from '../../hooks/useCustomPages'; import { useFqn } from '../../hooks/useFqn'; +import { useLazyEntityExtension } from '../../hooks/useLazyEntityExtension'; import { useSub } from '../../hooks/usePubSub'; import { FeedCounts } from '../../interface/feed.interface'; import { fetchTestCaseResultByTestSuiteId } from '../../rest/dataQualityDashboardAPI'; @@ -79,7 +79,6 @@ import { getTabLabelMapFromTabs, } from '../../utils/CustomizePage/CustomizePageUtils'; import { - customPropertiesFields, defaultFields, defaultFieldsWithColumns, } from '../../utils/DatasetDetailsUtils'; @@ -235,37 +234,19 @@ const TableDetailsPageV1: React.FC = () => { [tableFqn, viewUsagePermission] ); - // 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, + // Lazy custom-properties fetch — see {@link useLazyEntityExtension} for rationale and + // the P3.1 React Query pilot pattern. Eager `defaultFieldsWithColumns` deliberately omits + // `extension` because the blob can be hundreds of KB on tables with many user-defined + // properties and only the Custom Properties tab consumes it. + useLazyEntityExtension({ + entityType: EntityType.TABLE, + fqn: tableFqn, + activeTab: isTourOpen ? undefined : activeTab, + fetcher: getTableDetailsByFQN, + onResolve: (extension) => + setTableDetails((prev) => (prev ? { ...prev, extension } : prev)), }); - useEffect(() => { - if (!extensionResult) { - return; - } - setTableDetails((prev) => - prev ? { ...prev, extension: extensionResult.extension } : prev - ); - }, [extensionResult]); - const fetchDQUpstreamFailureCount = async () => { if (!tableClassBase.getAlertEnableStatus()) { setDqFailureCount(0); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx index 4ce69e41a22d..391decc33af1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx @@ -41,8 +41,11 @@ const EntityLineageTab = lazy(() => ) ); +// `EXTENSION` (custom-property values) is fetched lazily by {@link useLazyEntityExtension} +// when the user activates the Custom Properties tab — it can run into hundreds of KB on +// dashboards with many user-defined properties and only that tab consumes it. // eslint-disable-next-line max-len -export const defaultFields = `${TabSpecificField.DOMAINS},${TabSpecificField.OWNERS}, ${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.CHARTS},${TabSpecificField.VOTES},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.EXTENSION}`; +export const defaultFields = `${TabSpecificField.DOMAINS},${TabSpecificField.OWNERS}, ${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.CHARTS},${TabSpecificField.VOTES},${TabSpecificField.DATA_PRODUCTS}`; export const fetchCharts = async ( charts: Dashboard['charts'], diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DatasetDetailsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DatasetDetailsUtils.ts index 693a7a873adc..375d771e9656 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DatasetDetailsUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DatasetDetailsUtils.ts @@ -14,8 +14,8 @@ import { TabSpecificField } from '../enums/entity.enum'; // 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` (custom-property values — only the Custom Properties tab consumes this; the +// {@link useLazyEntityExtension} hook fetches it lazily on tab activation). 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 @@ -25,8 +25,4 @@ export const defaultFields = `${TabSpecificField.FOLLOWERS},${TabSpecificField.J // 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}`; -// 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}`; - export const commonTableFields = `${TabSpecificField.TAGS},${TabSpecificField.OWNERS},${TabSpecificField.DOMAINS},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.CERTIFICATION}`; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DirectoryDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DirectoryDetailsUtils.tsx index 8c258a0f4a97..5fe0689a0d6d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DirectoryDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DirectoryDetailsUtils.tsx @@ -35,6 +35,8 @@ export interface DirectoryDetailPageTabProps { labelMap?: Record; } +// `EXTENSION` (custom-property values) is fetched lazily by {@link useLazyEntityExtension} +// when the user activates the Custom Properties tab. See DatasetDetailsUtils.ts for context. export const defaultFields = [ TabSpecificField.OWNERS, TabSpecificField.CHILDREN, @@ -43,7 +45,6 @@ export const defaultFields = [ TabSpecificField.DOMAINS, TabSpecificField.DATA_PRODUCTS, TabSpecificField.VOTES, - TabSpecificField.EXTENSION, TabSpecificField.DIRECTORY_TYPE, TabSpecificField.NUMBER_OF_FILES, TabSpecificField.NUMBER_OF_SUB_DIRECTORIES, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.tsx index ad0b56190d2a..c9d81841b052 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MlModelDetailsUtils.tsx @@ -37,8 +37,10 @@ const EntityLineageTab = lazy(() => ) ); +// `EXTENSION` (custom-property values) is fetched lazily by {@link useLazyEntityExtension} +// when the user activates the Custom Properties tab. See DatasetDetailsUtils.ts for context. // eslint-disable-next-line max-len -export const defaultFields = `${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.DOMAINS},${TabSpecificField.OWNERS}, ${TabSpecificField.DASHBOARD},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.VOTES},${TabSpecificField.EXTENSION}`; +export const defaultFields = `${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.DOMAINS},${TabSpecificField.OWNERS}, ${TabSpecificField.DASHBOARD},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.VOTES}`; export const getMlModelDetailsPageTabs = ({ feedCount, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx index add5289066fc..37bf41237b59 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.tsx @@ -46,8 +46,11 @@ const EntityLineageTab = lazy(() => ) ); +// `EXTENSION` (custom-property values) is fetched lazily by {@link useLazyEntityExtension} +// when the user activates the Custom Properties tab. See DatasetDetailsUtils.ts for the +// rationale behind this trim — same logic, applied here. // eslint-disable-next-line max-len -export const defaultFields = `${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.OWNERS},${TabSpecificField.TASKS}, ${TabSpecificField.PIPELINE_STATUS}, ${TabSpecificField.DOMAINS},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.VOTES},${TabSpecificField.EXTENSION}`; +export const defaultFields = `${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.OWNERS},${TabSpecificField.TASKS}, ${TabSpecificField.PIPELINE_STATUS}, ${TabSpecificField.DOMAINS},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.VOTES}`; export const getTaskExecStatus = (taskName: string, tasks: TaskStatus[]) => { return tasks.find((task) => task.name === taskName)?.executionStatus; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx index 65db1b4f129e..3ee46d3d1ab8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchIndexUtils.tsx @@ -44,8 +44,10 @@ const EntityLineageTab = lazy(() => ) ); +// `EXTENSION` (custom-property values) is fetched lazily by {@link useLazyEntityExtension} +// when the user activates the Custom Properties tab. See DatasetDetailsUtils.ts for context. // eslint-disable-next-line max-len -export const defaultFields = `${TabSpecificField.FIELDS},${TabSpecificField.FOLLOWERS},${TabSpecificField.TAGS},${TabSpecificField.OWNERS},${TabSpecificField.DOMAINS},${TabSpecificField.VOTES},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.EXTENSION}`; +export const defaultFields = `${TabSpecificField.FIELDS},${TabSpecificField.FOLLOWERS},${TabSpecificField.TAGS},${TabSpecificField.OWNERS},${TabSpecificField.DOMAINS},${TabSpecificField.VOTES},${TabSpecificField.DATA_PRODUCTS}`; export const makeData = ( columns: SearchIndexField[] = [] diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SpreadsheetDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SpreadsheetDetailsUtils.tsx index 65125b8feedf..e49e18ae655b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SpreadsheetDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SpreadsheetDetailsUtils.tsx @@ -35,6 +35,8 @@ export interface SpreadsheetDetailPageTabProps { labelMap?: Record; } +// `EXTENSION` (custom-property values) is fetched lazily by {@link useLazyEntityExtension} +// when the user activates the Custom Properties tab. See DatasetDetailsUtils.ts for context. export const defaultFields = [ TabSpecificField.OWNERS, TabSpecificField.WORKSHEETS, @@ -43,7 +45,6 @@ export const defaultFields = [ TabSpecificField.DOMAINS, TabSpecificField.DATA_PRODUCTS, TabSpecificField.VOTES, - TabSpecificField.EXTENSION, TabSpecificField.MIME_TYPE, TabSpecificField.CREATED_TIME, TabSpecificField.MODIFIED_TIME, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx index 4bdf01f49632..40512da6072c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StoredProceduresUtils.tsx @@ -34,8 +34,10 @@ const EntityLineageTab = lazy(() => ) ); +// `EXTENSION` (custom-property values) is fetched lazily by {@link useLazyEntityExtension} +// when the user activates the Custom Properties tab. See DatasetDetailsUtils.ts for context. // eslint-disable-next-line max-len -export const STORED_PROCEDURE_DEFAULT_FIELDS = `${TabSpecificField.OWNERS}, ${TabSpecificField.FOLLOWERS},${TabSpecificField.TAGS}, ${TabSpecificField.DOMAINS},${TabSpecificField.DATA_PRODUCTS}, ${TabSpecificField.VOTES},${TabSpecificField.EXTENSION}`; +export const STORED_PROCEDURE_DEFAULT_FIELDS = `${TabSpecificField.OWNERS}, ${TabSpecificField.FOLLOWERS},${TabSpecificField.TAGS}, ${TabSpecificField.DOMAINS},${TabSpecificField.DATA_PRODUCTS}, ${TabSpecificField.VOTES}`; export const getStoredProcedureDetailsPageTabs = ({ activeTab, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/WorksheetDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/WorksheetDetailsUtils.tsx index 8ab775988535..c07634a60767 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/WorksheetDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/WorksheetDetailsUtils.tsx @@ -34,6 +34,8 @@ export interface WorksheetDetailPageTabProps { labelMap?: Record; } +// `EXTENSION` (custom-property values) is fetched lazily by {@link useLazyEntityExtension} +// when the user activates the Custom Properties tab. See DatasetDetailsUtils.ts for context. export const defaultFields = [ TabSpecificField.OWNERS, TabSpecificField.FOLLOWERS, @@ -41,7 +43,6 @@ export const defaultFields = [ TabSpecificField.DOMAINS, TabSpecificField.DATA_PRODUCTS, TabSpecificField.VOTES, - TabSpecificField.EXTENSION, TabSpecificField.ROW_COUNT, TabSpecificField.COLUMNS, TabSpecificField.ROW_COUNT, From 21acb63534916238e859cf4ef5eab41adc1e78eb Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Sun, 10 May 2026 10:53:52 -0700 Subject: [PATCH 4/6] perf(ui-perf): wire useLazyEntityExtension on Drive entity pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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(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`. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../DirectoryDetailsPage.tsx | 21 +++++++++++++++- .../SpreadsheetDetailsPage.tsx | 24 ++++++++++++++++++- .../WorksheetDetailsPage.tsx | 20 +++++++++++++++- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DirectoryDetailsPage/DirectoryDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DirectoryDetailsPage/DirectoryDetailsPage.tsx index 040fd8898052..71424d5b55be 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DirectoryDetailsPage/DirectoryDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DirectoryDetailsPage/DirectoryDetailsPage.tsx @@ -31,11 +31,17 @@ import { } from '../../context/PermissionProvider/PermissionProvider.interface'; import { ClientErrors } from '../../enums/Axios.enum'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; -import { EntityType, TabSpecificField } from '../../enums/entity.enum'; +import { + EntityTabs, + EntityType, + TabSpecificField, +} from '../../enums/entity.enum'; import { Directory } from '../../generated/entity/data/directory'; import { Operation as PermissionOperation } from '../../generated/entity/policies/accessControl/resourcePermission'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; +import { useLazyEntityExtension } from '../../hooks/useLazyEntityExtension'; +import { useRequiredParams } from '../../utils/useRequiredParams'; import { addDriveAssetFollower, getDriveAssetByFqn, @@ -64,9 +70,22 @@ const DirectoryDetailsPage = () => { const { getEntityPermissionByFqn } = usePermissionProvider(); const { fqn: directoryFQN } = useFqn(); + const { tab: activeTab } = useRequiredParams<{ tab: EntityTabs }>(); const [directoryDetails, setDirectoryDetails] = useState( {} as Directory ); + + // Lazy custom-properties fetch — see {@link useLazyEntityExtension}. + // getDriveAssetByFqn has a different signature (entityType-keyed) so we adapt it. + useLazyEntityExtension({ + entityType: EntityType.DIRECTORY, + fqn: directoryFQN, + activeTab, + fetcher: (fqn, params) => + getDriveAssetByFqn(fqn, EntityType.DIRECTORY, params.fields), + onResolve: (extension) => + setDirectoryDetails((prev) => ({ ...prev, extension })), + }); const [isLoading, setLoading] = useState(true); const [isError, setIsError] = useState(false); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SpreadsheetDetailsPage/SpreadsheetDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SpreadsheetDetailsPage/SpreadsheetDetailsPage.tsx index 1022ea8bbdda..5020aa235917 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SpreadsheetDetailsPage/SpreadsheetDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SpreadsheetDetailsPage/SpreadsheetDetailsPage.tsx @@ -31,11 +31,17 @@ import { } from '../../context/PermissionProvider/PermissionProvider.interface'; import { ClientErrors } from '../../enums/Axios.enum'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; -import { EntityType, TabSpecificField } from '../../enums/entity.enum'; +import { + EntityTabs, + EntityType, + TabSpecificField, +} from '../../enums/entity.enum'; import { Spreadsheet } from '../../generated/entity/data/spreadsheet'; import { Operation as PermissionOperation } from '../../generated/entity/policies/accessControl/resourcePermission'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; +import { useLazyEntityExtension } from '../../hooks/useLazyEntityExtension'; +import { useRequiredParams } from '../../utils/useRequiredParams'; import { addDriveAssetFollower, getDriveAssetByFqn, @@ -64,9 +70,25 @@ const SpreadsheetDetailsPage = () => { const { getEntityPermissionByFqn } = usePermissionProvider(); const { fqn: spreadsheetFQN } = useFqn(); + const { tab: activeTab } = useRequiredParams<{ tab: EntityTabs }>(); const [spreadsheetDetails, setSpreadsheetDetails] = useState( {} as Spreadsheet ); + + // Lazy custom-properties fetch — see {@link useLazyEntityExtension}. + useLazyEntityExtension({ + entityType: EntityType.SPREADSHEET, + fqn: spreadsheetFQN, + activeTab, + fetcher: (fqn, params) => + getDriveAssetByFqn( + fqn, + EntityType.SPREADSHEET, + params.fields + ), + onResolve: (extension) => + setSpreadsheetDetails((prev) => ({ ...prev, extension })), + }); const [isLoading, setLoading] = useState(true); const [isError, setIsError] = useState(false); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/WorksheetDetailsPage/WorksheetDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/WorksheetDetailsPage/WorksheetDetailsPage.tsx index cd9a0711a4d1..c460fe6f1d70 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/WorksheetDetailsPage/WorksheetDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/WorksheetDetailsPage/WorksheetDetailsPage.tsx @@ -32,11 +32,17 @@ import { } from '../../context/PermissionProvider/PermissionProvider.interface'; import { ClientErrors } from '../../enums/Axios.enum'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; -import { EntityType, TabSpecificField } from '../../enums/entity.enum'; +import { + EntityTabs, + EntityType, + TabSpecificField, +} from '../../enums/entity.enum'; import { Worksheet } from '../../generated/entity/data/worksheet'; import { Operation as PermissionOperation } from '../../generated/entity/policies/accessControl/resourcePermission'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; +import { useLazyEntityExtension } from '../../hooks/useLazyEntityExtension'; +import { useRequiredParams } from '../../utils/useRequiredParams'; import { addDriveAssetFollower, getDriveAssetByFqn, @@ -66,9 +72,21 @@ const WorksheetDetailsPage = () => { const { getEntityPermissionByFqn } = usePermissionProvider(); const { fqn: decodedWorksheetFQN } = useFqn(); + const { tab: activeTab } = useRequiredParams<{ tab: EntityTabs }>(); const [worksheetDetails, setWorksheetDetails] = useState( {} as Worksheet ); + + // Lazy custom-properties fetch — see {@link useLazyEntityExtension}. + useLazyEntityExtension({ + entityType: EntityType.WORKSHEET, + fqn: decodedWorksheetFQN, + activeTab, + fetcher: (fqn, params) => + getDriveAssetByFqn(fqn, EntityType.WORKSHEET, params.fields), + onResolve: (extension) => + setWorksheetDetails((prev) => ({ ...prev, extension })), + }); const [isLoading, setLoading] = useState(true); const [isError, setIsError] = useState(false); const [resolvedEntityFqn, setResolvedEntityFqn] = useState(''); From 516a881150359879f292a5e2a516edd5f81cdaf8 Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Sun, 10 May 2026 11:28:37 -0700 Subject: [PATCH 5/6] perf(ui-perf): migrate TableDetailsPageV1 main entity fetch to useQuery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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
()` + `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) --- .../TableDetailsPageV1/TableDetailsPageV1.tsx | 201 ++++++++++++------ 1 file changed, 140 insertions(+), 61 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index 43fa12aa630a..5ec0df07b318 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -11,6 +11,7 @@ * limitations under the License. */ +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { Col, Row, Tabs, Tooltip } from 'antd'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; @@ -109,7 +110,7 @@ const TableDetailsPageV1: React.FC = () => { useTourProvider(); const { currentUser } = useApplicationStore(); const { setDqLineageData } = useTestCaseStore(); - const [tableDetails, setTableDetails] = useState
(); + const queryClient = useQueryClient(); const { tab: activeTab } = useRequiredParams<{ tab: EntityTabs }>(); const { t } = useTranslation(); const navigate = useNavigate(); @@ -121,7 +122,6 @@ const TableDetailsPageV1: React.FC = () => { const [queryCount, setQueryCount] = useState(0); - const [loading, setLoading] = useState(!isTourOpen); const [tablePermissions, setTablePermissions] = useState( DEFAULT_ENTITY_PERMISSION ); @@ -157,20 +157,6 @@ const TableDetailsPageV1: React.FC = () => { ) : undefined; }, [dqFailureCount, tableFqn]); - const extraDropdownContent = useMemo( - () => - tableDetails - ? entityUtilClassBase.getManageExtraOptions( - EntityType.TABLE, - tableFqn, - tablePermissions, - tableDetails, - navigate - ) - : [], - [tablePermissions, tableFqn, tableDetails] - ); - const { viewUsagePermission, viewTestCasePermission } = useMemo( () => ({ viewUsagePermission: getPrioritizedViewPermission( @@ -189,49 +175,144 @@ const TableDetailsPageV1: React.FC = () => { ] ); - const isViewTableType = useMemo( - () => tableDetails?.tableType === TableType.View, - [tableDetails?.tableType] + // Composed `fields=` value, derived from permissions. USAGE_SUMMARY / TESTSUITE are only + // appended when the caller can read them — drives both the queryKey identity and the + // queryFn payload, so changing permissions automatically invalidates the cache for the + // wrong-shape entry and refetches. + const tableQueryFields = useMemo(() => { + let fields: string = defaultFieldsWithColumns; + if (viewUsagePermission) { + fields += `,${TabSpecificField.USAGE_SUMMARY}`; + } + if (viewTestCasePermission) { + fields += `,${TabSpecificField.TESTSUITE}`; + } + + return fields; + }, [viewUsagePermission, viewTestCasePermission]); + + // Stable React Query key. Includes the FQN and the fields string so that: + // * navigating from one table to another swaps queryKey, picking up the new cache slot + // (or refetching on miss); + // * a permission change that mutates the fields string invalidates the existing entry + // because the key shape is different. + const tableQueryKey = useMemo( + () => ['table-detail', tableFqn, tableQueryFields] as const, + [tableFqn, tableQueryFields] + ); + + // Permissions are loaded asynchronously by `fetchResourcePermission`. Until that resolves, + // `tablePermissions` is the sentinel `DEFAULT_ENTITY_PERMISSION` reference. We use that as + // a "permissions still loading" signal — gates both the query and the page-level loader so + // the page doesn't race the permission fetch. + const tablePermissionsLoaded = tablePermissions !== DEFAULT_ENTITY_PERMISSION; + + // 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 { + data: tableDetails, + isLoading: isTableLoading, + error: tableQueryError, + refetch: refetchTable, + } = useQuery({ + queryKey: tableQueryKey, + queryFn: () => getTableDetailsByFQN(tableFqn, { fields: tableQueryFields }), + enabled: + !isTourOpen && !isTourPage && tablePermissionsLoaded && Boolean(tableFqn), + }); + + // Bridge: existing call sites do `setTableDetails((prev) => ({...prev, ...patch}))` or + // `setTableDetails(newTable)`. Both forms still work because we forward to + // `queryClient.setQueryData`, which accepts a plain value or an updater closure. Keeping + // this wrapper means the ~25 mutation call sites in this file (edit handlers, follow, + // vote, restore, certification, tier, …) need no changes. + const setTableDetails = useCallback( + ( + next: Table | undefined | ((prev: Table | undefined) => Table | undefined) + ) => { + queryClient.setQueryData
(tableQueryKey, (prev) => + typeof next === 'function' + ? (next as (prev: Table | undefined) => Table | undefined)(prev) + : next + ); + }, + [queryClient, tableQueryKey] ); + // Bridge: `fetchTableDetails(showLoading?)` used to be a force-refetch helper. With React + // Query, the equivalent is `refetch()` — `isFetching` flips during the refetch but + // `isLoading` stays `false` after first success, which matches the old `showLoading=false` + // semantics naturally. The argument is now ignored. const fetchTableDetails = useCallback( - async (showLoading = true) => { - if (showLoading) { - setLoading(true); - } - try { - let fields = defaultFieldsWithColumns; - if (viewUsagePermission) { - fields += `,${TabSpecificField.USAGE_SUMMARY}`; - } - if (viewTestCasePermission) { - fields += `,${TabSpecificField.TESTSUITE}`; - } + () => refetchTable().then(() => undefined), + [refetchTable] + ); - const tableDetails = await getTableDetailsByFQN(tableFqn, { fields }); + // Surface a loader while permissions are still loading too — otherwise the page briefly + // renders the "no data" placeholder before the query is even enabled. + const loading = + !isTourOpen && !isTourPage && (!tablePermissionsLoaded || isTableLoading); - setTableDetails(tableDetails); - addToRecentViewed({ - displayName: getEntityName(tableDetails), - entityType: EntityType.TABLE, - fqn: tableDetails.fullyQualifiedName ?? '', - serviceType: tableDetails.serviceType, - timestamp: 0, - id: tableDetails.id, - }); - } catch (error) { - if ( - (error as AxiosError)?.response?.status === ClientErrors.FORBIDDEN - ) { - navigate(ROUTES.FORBIDDEN, { replace: true }); - } - } finally { - if (showLoading) { - setLoading(false); - } - } - }, - [tableFqn, viewUsagePermission] + // Forbidden navigation — used to live in fetchTableDetails' catch. React Query surfaces + // the same error via `error`; redirect on FORBIDDEN once. + useEffect(() => { + if ( + tableQueryError && + (tableQueryError as AxiosError)?.response?.status === + ClientErrors.FORBIDDEN + ) { + navigate(ROUTES.FORBIDDEN, { replace: true }); + } + }, [tableQueryError, navigate]); + + // Recently-viewed bookkeeping — used to live in fetchTableDetails' success path. Now an + // effect on the freshly-resolved entity id. + useEffect(() => { + if (!tableDetails || isTourOpen || isTourPage) { + return; + } + addToRecentViewed({ + displayName: getEntityName(tableDetails), + entityType: EntityType.TABLE, + fqn: tableDetails.fullyQualifiedName ?? '', + serviceType: tableDetails.serviceType, + timestamp: 0, + id: tableDetails.id, + }); + }, [tableDetails?.id, isTourOpen, isTourPage]); + + // Tour mode — prime the cache with mock data so consumers (which read from the cache via + // `tableDetails`) see the mock entity. Replaces a `setTableDetails(mock)` call from the + // legacy useEffect. + useEffect(() => { + if (isTourOpen || isTourPage) { + queryClient.setQueryData( + tableQueryKey, + mockDatasetData.tableDetails as unknown as Table + ); + } + }, [isTourOpen, isTourPage, queryClient, tableQueryKey]); + + const isViewTableType = useMemo( + () => tableDetails?.tableType === TableType.View, + [tableDetails?.tableType] + ); + + const extraDropdownContent = useMemo( + () => + tableDetails + ? entityUtilClassBase.getManageExtraOptions( + EntityType.TABLE, + tableFqn, + tablePermissions, + tableDetails, + navigate + ) + : [], + [tablePermissions, tableFqn, tableDetails, navigate] ); // Lazy custom-properties fetch — see {@link useLazyEntityExtension} for rationale and @@ -366,8 +447,6 @@ const TableDetailsPageV1: React.FC = () => { entity: t('label.resource-permission-lowercase'), }) ); - } finally { - setLoading(false); } }, [getEntityPermissionByFqn, setTablePermissions] @@ -789,12 +868,12 @@ const TableDetailsPageV1: React.FC = () => { [] ); + // Tour-mode mock priming and the table fetch itself now both live with the React Query + // setup above. This effect remains to drive the *non-table* side-effect — `getEntityFeedCount` + // — that used to be co-located with the legacy fetch on FQN change. Kept gated on + // `viewBasicPermission` so we don't issue a feed-count fetch the user isn't allowed to see. useEffect(() => { - if (isTourOpen || isTourPage) { - setTableDetails(mockDatasetData.tableDetails as unknown as Table); - } else if (viewBasicPermission) { - setTableDetails(undefined); - fetchTableDetails(); + if (!isTourOpen && !isTourPage && viewBasicPermission) { getEntityFeedCount(); } }, [tableFqn, isTourOpen, isTourPage, viewBasicPermission]); From 6768c3e06be4e861370e1709e3ddaecb77528337 Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Wed, 13 May 2026 13:08:11 -0700 Subject: [PATCH 6/6] =?UTF-8?q?fix(ui-perf):=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20logout=20cache=20wipe,=20permission=20gate,=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Auth/AuthProviders/AuthProvider.tsx | 5 + .../DashboardDetailsPage.component.tsx | 2 +- .../DashboardDetailsPage.test.tsx | 2 +- .../DirectoryDetailsPage.tsx | 2 +- .../MlModelPage/MlModelPage.component.tsx | 2 +- .../PipelineDetailsPage.component.tsx | 2 +- .../SearchIndexDetailsPage.test.tsx | 9 +- .../SpreadsheetDetailsPage.test.tsx | 2 +- .../SpreadsheetDetailsPage.tsx | 2 +- .../TableDetailsPageV1.test.tsx | 114 ++++++------------ .../TableDetailsPageV1/TableDetailsPageV1.tsx | 66 +++++----- .../WorksheetDetailsPage.test.tsx | 2 +- .../WorksheetDetailsPage.tsx | 2 +- .../src/main/resources/ui/src/setupTests.js | 12 ++ 14 files changed, 106 insertions(+), 118 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx index 74ecf1275491..451991bf74fe 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx @@ -53,6 +53,7 @@ import { AuthProvider as AuthProviderEnum } from '../../../generated/settings/se import { withDomainFilter } from '../../../hoc/withDomainFilter'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import useCustomLocation from '../../../hooks/useCustomLocation/useCustomLocation'; +import { queryClient } from '../../../queryClient'; import axiosClient from '../../../rest'; import { fetchAuthenticationConfig, @@ -223,6 +224,10 @@ export const AuthProvider = ({ // Clear the refresh flag (used after refresh is complete) tokenService.current.clearRefreshInProgress(); + // Wipe React Query cache so cached responses from the previous user do not bleed into + // the next user's session on shared machines. + queryClient.clear(); + // Upon logout, redirect to the login page navigate(ROUTES.SIGNIN); }, [timeoutId]); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx index 42629e9dcc90..3c3bb03cb316 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx @@ -38,7 +38,6 @@ import { Operation as PermissionOperation } from '../../generated/entity/policie import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { useLazyEntityExtension } from '../../hooks/useLazyEntityExtension'; -import { useRequiredParams } from '../../utils/useRequiredParams'; import { addFollower, getDashboardByFqn, @@ -58,6 +57,7 @@ import { } from '../../utils/PermissionsUtils'; import { getVersionPath } from '../../utils/RouterUtils'; import { showErrorToast } from '../../utils/ToastUtils'; +import { useRequiredParams } from '../../utils/useRequiredParams'; export type ChartType = { displayName: string; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.test.tsx index 2419b911e46e..d371cfa10104 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.test.tsx @@ -105,7 +105,7 @@ describe('DashboardDetailsPage', () => { expect(getDashboardByFqn).toHaveBeenCalledWith('test-dashboard', { fields: - 'domains,owners, followers, tags, charts,votes,dataProducts,extension,usageSummary', + 'domains,owners, followers, tags, charts,votes,dataProducts,usageSummary', }); expect(screen.getByTestId('no-data-placeholder')).toBeInTheDocument(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DirectoryDetailsPage/DirectoryDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DirectoryDetailsPage/DirectoryDetailsPage.tsx index 71424d5b55be..26b60676e54d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DirectoryDetailsPage/DirectoryDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DirectoryDetailsPage/DirectoryDetailsPage.tsx @@ -41,7 +41,6 @@ import { Operation as PermissionOperation } from '../../generated/entity/policie import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { useLazyEntityExtension } from '../../hooks/useLazyEntityExtension'; -import { useRequiredParams } from '../../utils/useRequiredParams'; import { addDriveAssetFollower, getDriveAssetByFqn, @@ -61,6 +60,7 @@ import { } from '../../utils/PermissionsUtils'; import { getVersionPath } from '../../utils/RouterUtils'; import { showErrorToast } from '../../utils/ToastUtils'; +import { useRequiredParams } from '../../utils/useRequiredParams'; const DirectoryDetailsPage = () => { const { t } = useTranslation(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx index 81b9a296beee..dee084635dcf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/MlModelPage/MlModelPage.component.tsx @@ -37,7 +37,6 @@ import { Operation as PermissionOperation } from '../../generated/entity/policie import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { useLazyEntityExtension } from '../../hooks/useLazyEntityExtension'; -import { useRequiredParams } from '../../utils/useRequiredParams'; import { addFollower, getMlModelByFQN, @@ -57,6 +56,7 @@ import { } from '../../utils/PermissionsUtils'; import { getVersionPath } from '../../utils/RouterUtils'; import { showErrorToast } from '../../utils/ToastUtils'; +import { useRequiredParams } from '../../utils/useRequiredParams'; const MlModelPage = () => { const { t } = useTranslation(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/PipelineDetails/PipelineDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/PipelineDetails/PipelineDetailsPage.component.tsx index 4f1befd9c226..363c706421c4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/PipelineDetails/PipelineDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/PipelineDetails/PipelineDetailsPage.component.tsx @@ -38,7 +38,6 @@ import { Paging } from '../../generated/type/paging'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { useLazyEntityExtension } from '../../hooks/useLazyEntityExtension'; -import { useRequiredParams } from '../../utils/useRequiredParams'; import { addFollower, getPipelineByFqn, @@ -58,6 +57,7 @@ import { import { defaultFields } from '../../utils/PipelineDetailsUtils'; import { getVersionPath } from '../../utils/RouterUtils'; import { showErrorToast } from '../../utils/ToastUtils'; +import { useRequiredParams } from '../../utils/useRequiredParams'; const PipelineDetailsPage = () => { const { t } = useTranslation(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.test.tsx index 5ae487ba0a6b..53078a5e87ce 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SearchIndexDetailsPage/SearchIndexDetailsPage.test.tsx @@ -224,8 +224,7 @@ describe('SearchIndexDetailsPage component', () => { expect(getSearchIndexDetailsByFQN).toHaveBeenCalledWith( 'test-service.test-search-index', { - fields: - 'fields,followers,tags,owners,domains,votes,dataProducts,extension', + fields: 'fields,followers,tags,owners,domains,votes,dataProducts', } ); }, @@ -253,8 +252,7 @@ describe('SearchIndexDetailsPage component', () => { expect(getSearchIndexDetailsByFQN).toHaveBeenCalledWith( 'test-service.test-search-index', { - fields: - 'fields,followers,tags,owners,domains,votes,dataProducts,extension', + fields: 'fields,followers,tags,owners,domains,votes,dataProducts', } ); }, @@ -287,8 +285,7 @@ describe('SearchIndexDetailsPage component', () => { expect(getSearchIndexDetailsByFQN).toHaveBeenCalledWith( 'test-service.test-search-index', { - fields: - 'fields,followers,tags,owners,domains,votes,dataProducts,extension', + fields: 'fields,followers,tags,owners,domains,votes,dataProducts', } ); }, diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SpreadsheetDetailsPage/SpreadsheetDetailsPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SpreadsheetDetailsPage/SpreadsheetDetailsPage.test.tsx index 5f77cf94e3fc..11fe08f9e092 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SpreadsheetDetailsPage/SpreadsheetDetailsPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SpreadsheetDetailsPage/SpreadsheetDetailsPage.test.tsx @@ -367,7 +367,7 @@ describe('SpreadsheetDetailsPage', () => { expect(getDriveAssetByFqn).toHaveBeenCalledWith( 'test-service.test-spreadsheet', EntityType.SPREADSHEET, - 'owners,worksheets,followers,tags,domains,dataProducts,votes,extension,mimeType,createdTime,modifiedTime' + 'owners,worksheets,followers,tags,domains,dataProducts,votes,mimeType,createdTime,modifiedTime' ); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/SpreadsheetDetailsPage/SpreadsheetDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/SpreadsheetDetailsPage/SpreadsheetDetailsPage.tsx index 5020aa235917..0612489fd3a1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/SpreadsheetDetailsPage/SpreadsheetDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/SpreadsheetDetailsPage/SpreadsheetDetailsPage.tsx @@ -41,7 +41,6 @@ import { Operation as PermissionOperation } from '../../generated/entity/policie import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { useLazyEntityExtension } from '../../hooks/useLazyEntityExtension'; -import { useRequiredParams } from '../../utils/useRequiredParams'; import { addDriveAssetFollower, getDriveAssetByFqn, @@ -61,6 +60,7 @@ import { import { getVersionPath } from '../../utils/RouterUtils'; import { defaultFields } from '../../utils/SpreadsheetDetailsUtils'; import { showErrorToast } from '../../utils/ToastUtils'; +import { useRequiredParams } from '../../utils/useRequiredParams'; const SpreadsheetDetailsPage = () => { const { t } = useTranslation(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.test.tsx index 960f94e209d0..9b85cdeaa8c7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.test.tsx @@ -10,8 +10,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { act, render, screen } from '@testing-library/react'; -import React from 'react'; +import React, { ReactElement } from 'react'; import { MemoryRouter } from 'react-router-dom'; import { GenericTab } from '../../components/Customization/GenericTab/GenericTab'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; @@ -21,6 +22,19 @@ import { getTableDetailsByFQN } from '../../rest/tableAPI'; import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils'; import TableDetailsPageV1 from './TableDetailsPageV1'; +// Each render gets a fresh QueryClient so cache state never leaks between tests. +const renderWithProviders = (ui: ReactElement) => { + const testQueryClient = new QueryClient({ + defaultOptions: { queries: { retry: false, gcTime: 0 } }, + }); + + return render( + + {ui} + + ); +}; + /** * Mock MUI components that have Jest compatibility issues */ @@ -105,7 +119,7 @@ const mockEntityPermissionByFqn = jest .mockImplementation(() => DEFAULT_ENTITY_PERMISSION); const COMMON_API_FIELDS = - 'columns,followers,joins,tags,owners,dataModel,tableConstraints,schemaDefinition,domains,dataProducts,votes,extension'; + 'columns,followers,joins,tags,owners,dataModel,tableConstraints,schemaDefinition,domains,dataProducts,votes'; jest.mock('../../context/PermissionProvider/PermissionProvider', () => ({ usePermissionProvider: jest.fn().mockImplementation(() => ({ @@ -139,6 +153,7 @@ jest.mock('../../rest/suggestionsAPI', () => ({ })); jest.mock('../../utils/CommonUtils', () => ({ + addToRecentViewed: jest.fn(), getFeedCounts: jest.fn(), getPartialNameFromTableFQN: jest.fn().mockImplementation(() => 'fqn'), getTableFQNFromColumnFQN: jest.fn(), @@ -320,21 +335,13 @@ jest.mock( describe('TestDetailsPageV1 component', () => { it('TableDetailsPageV1 should fetch permissions', () => { - render( - - - - ); + renderWithProviders(); expect(mockEntityPermissionByFqn).toHaveBeenCalledWith('table', 'fqn'); }); it('TableDetailsPageV1 should not fetch table details if permission is there', () => { - render( - - - - ); + renderWithProviders(); expect(getTableDetailsByFQN).not.toHaveBeenCalled(); }); @@ -347,11 +354,7 @@ describe('TestDetailsPageV1 component', () => { })); await act(async () => { - render( - - - - ); + renderWithProviders(); }); expect(getTableDetailsByFQN).toHaveBeenCalledWith('fqn', { @@ -369,11 +372,7 @@ describe('TestDetailsPageV1 component', () => { })); await act(async () => { - render( - - - - ); + renderWithProviders(); }); expect(getTableDetailsByFQN).toHaveBeenCalledWith('fqn', { @@ -389,11 +388,7 @@ describe('TestDetailsPageV1 component', () => { })); await act(async () => { - render( - - - - ); + renderWithProviders(); }); expect(getTableDetailsByFQN).toHaveBeenCalledWith('fqn', { @@ -435,11 +430,7 @@ describe('TestDetailsPageV1 component', () => { ); await act(async () => { - render( - - - - ); + renderWithProviders(); }); expect(await screen.findByText('label.dbt-lowercase')).toBeInTheDocument(); @@ -464,11 +455,7 @@ describe('TestDetailsPageV1 component', () => { ); await act(async () => { - render( - - - - ); + renderWithProviders(); }); expect(await screen.findByText('label.dbt-lowercase')).toBeInTheDocument(); @@ -493,11 +480,7 @@ describe('TestDetailsPageV1 component', () => { ); await act(async () => { - render( - - - - ); + renderWithProviders(); }); expect(await screen.findByText('label.dbt-lowercase')).toBeInTheDocument(); @@ -522,11 +505,7 @@ describe('TestDetailsPageV1 component', () => { ); await act(async () => { - render( - - - - ); + renderWithProviders(); }); expect(await screen.findByText('label.dbt-lowercase')).toBeInTheDocument(); @@ -551,11 +530,7 @@ describe('TestDetailsPageV1 component', () => { ); await act(async () => { - render( - - - - ); + renderWithProviders(); }); expect(await screen.findByText('label.dbt-lowercase')).toBeInTheDocument(); @@ -579,14 +554,12 @@ describe('TestDetailsPageV1 component', () => { ); await act(async () => { - render( - - - - ); + renderWithProviders(); }); - expect(screen.getByText('label.schema-definition')).toBeInTheDocument(); + expect( + await screen.findByText('label.schema-definition') + ).toBeInTheDocument(); expect(screen.queryByText('label.dbt-lowercase')).not.toBeInTheDocument(); }); @@ -608,14 +581,12 @@ describe('TestDetailsPageV1 component', () => { ); await act(async () => { - render( - - - - ); + renderWithProviders(); }); - expect(screen.getByText('label.view-definition')).toBeInTheDocument(); + expect( + await screen.findByText('label.view-definition') + ).toBeInTheDocument(); }); it('TableDetailsPageV1 should render schemaTab by default', async () => { @@ -626,11 +597,7 @@ describe('TestDetailsPageV1 component', () => { })); await act(async () => { - render( - - - - ); + renderWithProviders(); }); expect(getTableDetailsByFQN).toHaveBeenCalledWith('fqn', { @@ -659,13 +626,12 @@ describe('TestDetailsPageV1 component', () => { ); await act(async () => { - render( - - - - ); + renderWithProviders(); }); + // Await query resolution so PageLayoutV1 has been called with the resolved entity. + expect(await screen.findByText('GenericTab')).toBeInTheDocument(); + expect(PageLayoutV1).toHaveBeenCalledWith( expect.objectContaining({ pageTitle: 'test-table', diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index 5ec0df07b318..2b3a925a8344 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -125,6 +125,7 @@ const TableDetailsPageV1: React.FC = () => { const [tablePermissions, setTablePermissions] = useState( DEFAULT_ENTITY_PERMISSION ); + const [tablePermissionsLoaded, setTablePermissionsLoaded] = useState(false); const [dqFailureCount, setDqFailureCount] = useState(0); const { customizedPage } = useCustomPages(PageType.Table); const [isTabExpanded, setIsTabExpanded] = useState(false); @@ -157,23 +158,24 @@ const TableDetailsPageV1: React.FC = () => { ) : undefined; }, [dqFailureCount, tableFqn]); - const { viewUsagePermission, viewTestCasePermission } = useMemo( - () => ({ - viewUsagePermission: getPrioritizedViewPermission( - tablePermissions, - Operation.ViewUsage - ), - viewTestCasePermission: getPrioritizedViewPermission( - tablePermissions, - Operation.ViewTests - ), - }), - [ - tablePermissions, - getPrioritizedViewPermission, - getPrioritizedEditPermission, - ] - ); + const { viewUsagePermission, viewTestCasePermission, viewBasicPermission } = + useMemo( + () => ({ + viewUsagePermission: getPrioritizedViewPermission( + tablePermissions, + Operation.ViewUsage + ), + viewTestCasePermission: getPrioritizedViewPermission( + tablePermissions, + Operation.ViewTests + ), + viewBasicPermission: getPrioritizedViewPermission( + tablePermissions, + Operation.ViewBasic + ), + }), + [tablePermissions] + ); // Composed `fields=` value, derived from permissions. USAGE_SUMMARY / TESTSUITE are only // appended when the caller can read them — drives both the queryKey identity and the @@ -201,17 +203,18 @@ const TableDetailsPageV1: React.FC = () => { [tableFqn, tableQueryFields] ); - // Permissions are loaded asynchronously by `fetchResourcePermission`. Until that resolves, - // `tablePermissions` is the sentinel `DEFAULT_ENTITY_PERMISSION` reference. We use that as - // a "permissions still loading" signal — gates both the query and the page-level loader so - // the page doesn't race the permission fetch. - const tablePermissionsLoaded = tablePermissions !== DEFAULT_ENTITY_PERMISSION; - // 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. + // + // `enabled` gates on: + // * `tablePermissionsLoaded` — wait for the async permission fetch before firing, so we + // have an accurate fields string and don't race the permission resolve. + // * `viewBasicPermission` — match legacy behaviour: don't issue a guaranteed-403 request + // for users who can't view the entity. The early-return placeholder branch below + // renders the permission error instead. const { data: tableDetails, isLoading: isTableLoading, @@ -221,7 +224,11 @@ const TableDetailsPageV1: React.FC = () => { queryKey: tableQueryKey, queryFn: () => getTableDetailsByFQN(tableFqn, { fields: tableQueryFields }), enabled: - !isTourOpen && !isTourPage && tablePermissionsLoaded && Boolean(tableFqn), + !isTourOpen && + !isTourPage && + tablePermissionsLoaded && + viewBasicPermission && + Boolean(tableFqn), }); // Bridge: existing call sites do `setTableDetails((prev) => ({...prev, ...patch}))` or @@ -447,11 +454,17 @@ const TableDetailsPageV1: React.FC = () => { entity: t('label.resource-permission-lowercase'), }) ); + } finally { + setTablePermissionsLoaded(true); } }, [getEntityPermissionByFqn, setTablePermissions] ); + useEffect(() => { + setTablePermissionsLoaded(false); + }, [tableFqn]); + useEffect(() => { if (tableFqn) { fetchResourcePermission(tableFqn); @@ -565,7 +578,6 @@ const TableDetailsPageV1: React.FC = () => { viewQueriesPermission, viewProfilerPermission, viewAllPermission, - viewBasicPermission, viewCustomPropertiesPermission, } = useMemo( () => ({ @@ -604,10 +616,6 @@ const TableDetailsPageV1: React.FC = () => { Operation.ViewDataProfile ), viewAllPermission: tablePermissions.ViewAll, - viewBasicPermission: getPrioritizedViewPermission( - tablePermissions, - Operation.ViewBasic - ), viewCustomPropertiesPermission: getPrioritizedViewPermission( tablePermissions, Operation.ViewCustomFields diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/WorksheetDetailsPage/WorksheetDetailsPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/WorksheetDetailsPage/WorksheetDetailsPage.test.tsx index d1ceb8fd17c2..69c98d085fdd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/WorksheetDetailsPage/WorksheetDetailsPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/WorksheetDetailsPage/WorksheetDetailsPage.test.tsx @@ -374,7 +374,7 @@ describe('WorksheetDetailsPage', () => { expect(getDriveAssetByFqn).toHaveBeenCalledWith( 'test-service.test-worksheet', EntityType.WORKSHEET, - 'owners,followers,tags,domains,dataProducts,votes,extension,rowCount,columns,rowCount' + 'owners,followers,tags,domains,dataProducts,votes,rowCount,columns,rowCount' ); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/WorksheetDetailsPage/WorksheetDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/WorksheetDetailsPage/WorksheetDetailsPage.tsx index c460fe6f1d70..f7d04409d351 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/WorksheetDetailsPage/WorksheetDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/WorksheetDetailsPage/WorksheetDetailsPage.tsx @@ -42,7 +42,6 @@ import { Operation as PermissionOperation } from '../../generated/entity/policie import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { useLazyEntityExtension } from '../../hooks/useLazyEntityExtension'; -import { useRequiredParams } from '../../utils/useRequiredParams'; import { addDriveAssetFollower, getDriveAssetByFqn, @@ -62,6 +61,7 @@ import { } from '../../utils/PermissionsUtils'; import { getVersionPath } from '../../utils/RouterUtils'; import { showErrorToast } from '../../utils/ToastUtils'; +import { useRequiredParams } from '../../utils/useRequiredParams'; import { defaultFields } from '../../utils/WorksheetDetailsUtils'; const WorksheetDetailsPage = () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/setupTests.js b/openmetadata-ui/src/main/resources/ui/src/setupTests.js index b7ff1e84e5e4..83a383d15f30 100644 --- a/openmetadata-ui/src/main/resources/ui/src/setupTests.js +++ b/openmetadata-ui/src/main/resources/ui/src/setupTests.js @@ -104,6 +104,18 @@ jest.mock('./utils/ToastUtils', () => ({ showErrorToast: jest.fn(), })); +/** + * Global mock for useLazyEntityExtension. The real hook calls `useQuery`, which + * requires a `QueryClientProvider` ancestor in the render tree. Entity-detail page + * tests render the page component directly without that provider; mock the hook + * to a no-op here so existing tests do not need to set up React Query plumbing + * just to render. Individual tests that exercise the lazy-extension behaviour + * can override this mock locally. + */ +jest.mock('./hooks/useLazyEntityExtension', () => ({ + useLazyEntityExtension: jest.fn(), +})); + jest.mock('./components/ActivityFeed/FeedEditor/FeedEditor.tsx', () => ({ FeedEditor: jest.fn().mockImplementation(() => 'FeedEditor'), }));