Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions openmetadata-ui/src/main/resources/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 11 additions & 3 deletions openmetadata-ui/src/main/resources/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<AuthProvider childComponentType={AppRouter}>
<AppRouter />
</AuthProvider>
<QueryClientProvider client={queryClient}>
<AuthProvider childComponentType={AppRouter}>
<AppRouter />
</AuthProvider>
</QueryClientProvider>
Comment on lines +26 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Security: QueryClient cache not cleared on logout leaks data between users

The QueryClientProvider is deliberately placed outside AuthProvider so the cache survives auth-flow remounts. However, there is no queryClient.clear() call in the logout handler. If user A logs out and user B logs in on the same browser tab, user B may briefly see user A's cached query results (e.g. the extension/custom-properties payload keyed by table FQN). This is a data-leakage vector in shared-machine environments.

The PR description justifies the placement for caching feature-flag fetches across auth transitions, but the trade-off needs an explicit cache wipe on credential change.

Suggested fix:

// In AuthProvider's onLogoutHandler (or a useEffect watching the user identity):
import { queryClient } from '../../queryClient';

const onLogoutHandler = async () => {
  // ... existing cleanup ...
  queryClient.clear(); // wipe all cached queries on logout
};
  • Apply suggested fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

Comment on lines 20 to +30
Comment on lines +21 to +30
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -78,6 +79,7 @@ import {
getTabLabelMapFromTabs,
} from '../../utils/CustomizePage/CustomizePageUtils';
import {
customPropertiesFields,
defaultFields,
defaultFieldsWithColumns,
} from '../../utils/DatasetDetailsUtils';
Expand Down Expand Up @@ -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;
Comment thread
gitar-bot[bot] marked this conversation as resolved.
Outdated
}
setTableDetails((prev) =>
prev ? { ...prev, extension: extensionResult.extension } : prev
);
}, [extensionResult]);

const fetchDQUpstreamFailureCount = async () => {
if (!tableClassBase.getAlertEnableStatus()) {
setDqFailureCount(0);
Expand Down
42 changes: 42 additions & 0 deletions openmetadata-ui/src/main/resources/ui/src/queryClient.ts
Original file line number Diff line number Diff line change
@@ -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,
},
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
17 changes: 14 additions & 3 deletions openmetadata-ui/src/main/resources/ui/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
Loading