diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.test.tsx
index c815a44fbb4c..63498e38b26f 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.test.tsx
@@ -52,6 +52,14 @@ jest.mock('../../common/EntityDescription/Description', () =>
jest.fn().mockImplementation(() =>
Description
)
);
+jest.mock('../../../hooks/useCustomPages', () => ({
+ useCustomPages: jest.fn().mockReturnValue({
+ customizedPage: null,
+ navigation: null,
+ isLoading: false,
+ }),
+}));
+
const mockProps = {
glossary: mockedGlossaries[0],
glossaryTerms: [],
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.test.tsx
index 8439fb85c040..0873e26b47a5 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.test.tsx
@@ -130,6 +130,14 @@ jest.mock(
})
);
+jest.mock('../../hooks/useCustomPages', () => ({
+ useCustomPages: jest.fn().mockReturnValue({
+ customizedPage: null,
+ navigation: null,
+ isLoading: false,
+ }),
+}));
+
const mockProps: GlossaryV1Props = {
selectedData: mockedGlossaries[0],
isGlossaryActive: true,
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebar.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebar.test.tsx
index cbe646a3b3a8..9df044fb1959 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebar.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/LeftSidebar/LeftSidebar.test.tsx
@@ -21,6 +21,14 @@ jest.mock(
})
);
+jest.mock('../../../hooks/useCustomPages', () => ({
+ useCustomPages: jest.fn().mockReturnValue({
+ customizedPage: null,
+ navigation: null,
+ isLoading: false,
+ }),
+}));
+
describe('LeftSidebar', () => {
it('renders sidebar links correctly', () => {
render(
diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomPages.test.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomPages.test.ts
index 3328bc2aec13..47073e7c9686 100644
--- a/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomPages.test.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomPages.test.ts
@@ -10,7 +10,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { renderHook } from '@testing-library/react-hooks';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { renderHook, waitFor } from '@testing-library/react';
+import React from 'react';
import { Document } from '../generated/entity/docStore/document';
import { PageType } from '../generated/system/ui/page';
import { getDocumentByFQN } from '../rest/DocStoreAPI';
@@ -32,7 +34,16 @@ jest.mock('../rest/DocStoreAPI', () => ({
getDocumentByFQN: jest.fn(),
}));
+const createWrapper = (queryClient: QueryClient) => {
+ const Wrapper = ({ children }: { children: React.ReactNode }) =>
+ React.createElement(QueryClientProvider, { client: queryClient }, children);
+
+ return Wrapper;
+};
+
describe('useCustomPages', () => {
+ let queryClient: QueryClient;
+
const mockSelectedPersona = {
fullyQualifiedName: 'test-persona',
};
@@ -60,6 +71,9 @@ describe('useCustomPages', () => {
};
beforeEach(() => {
+ queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
jest.clearAllMocks();
mockUseApplicationStore.mockReturnValue({
selectedPersona: mockSelectedPersona,
@@ -69,75 +83,92 @@ describe('useCustomPages', () => {
it('should fetch and return customized page and navigation when persona is selected', async () => {
mockGetDocumentByFQN.mockResolvedValue(mockDocument);
- const { result, waitForNextUpdate } = renderHook(() =>
- useCustomPages(PageType.Table)
- );
-
- expect(result.current.isLoading).toBe(true);
+ const { result } = renderHook(() => useCustomPages(PageType.Table), {
+ wrapper: createWrapper(queryClient),
+ });
- await waitForNextUpdate();
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false);
+ });
expect(mockGetDocumentByFQN).toHaveBeenCalledWith('persona.test-persona');
expect(result.current.customizedPage).toEqual(mockPage);
expect(result.current.navigation).toEqual(mockNavigation);
- expect(result.current.isLoading).toBe(false);
});
it('should handle error when fetching document fails', async () => {
mockGetDocumentByFQN.mockRejectedValue(new Error('API Error'));
- const { result, waitForNextUpdate } = renderHook(() =>
- useCustomPages(PageType.Table)
- );
-
- expect(result.current.isLoading).toBe(true);
+ const { result } = renderHook(() => useCustomPages(PageType.Table), {
+ wrapper: createWrapper(queryClient),
+ });
- await waitForNextUpdate();
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false);
+ });
expect(mockGetDocumentByFQN).toHaveBeenCalledWith('persona.test-persona');
expect(result.current.customizedPage).toBeNull();
expect(result.current.navigation).toEqual([]);
- expect(result.current.isLoading).toBe(false);
});
- it('should not fetch document when no persona is selected', () => {
+ it('should not fetch document when no persona is selected', async () => {
mockUseApplicationStore.mockReturnValue({
selectedPersona: null,
});
- const { result } = renderHook(() => useCustomPages(PageType.Table));
+ const { result } = renderHook(() => useCustomPages(PageType.Table), {
+ wrapper: createWrapper(queryClient),
+ });
expect(mockGetDocumentByFQN).not.toHaveBeenCalled();
expect(result.current.customizedPage).toBeNull();
expect(result.current.navigation).toBeNull();
- expect(result.current.isLoading).toBe(false);
+
+ // hasMounted starts false (isLoading = true), flips after the mount effect.
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false);
+ });
});
- it('should refetch document when pageType changes', async () => {
- mockGetDocumentByFQN.mockResolvedValue(mockDocument);
+ it('should filter by pageType from cached doc without re-fetching', async () => {
+ const mockDocWithMultiplePages: Document = {
+ ...mockDocument,
+ data: {
+ pages: [
+ { pageType: PageType.Table, tabs: [] },
+ { pageType: PageType.Dashboard, tabs: [] },
+ ],
+ navigation: mockNavigation,
+ },
+ };
+ mockGetDocumentByFQN.mockResolvedValue(mockDocWithMultiplePages);
- const { rerender, waitForNextUpdate } = renderHook(
- ({ pageType }) => useCustomPages(pageType),
+ const { result, rerender } = renderHook(
+ ({ pageType }: { pageType: PageType }) => useCustomPages(pageType),
{
initialProps: { pageType: PageType.Table },
+ wrapper: createWrapper(queryClient),
}
);
- await waitForNextUpdate();
+ await waitFor(() => {
+ expect(result.current.customizedPage?.pageType).toBe(PageType.Table);
+ });
+ // Changing pageType filters locally from the cached doc — no extra network request.
expect(mockGetDocumentByFQN).toHaveBeenCalledTimes(1);
rerender({ pageType: PageType.Dashboard });
- await waitForNextUpdate();
-
- expect(mockGetDocumentByFQN).toHaveBeenCalledTimes(2);
+ expect(mockGetDocumentByFQN).toHaveBeenCalledTimes(1);
+ expect(result.current.customizedPage?.pageType).toBe(PageType.Dashboard);
});
it('should return updated results when selected persona changes', async () => {
mockGetDocumentByFQN.mockResolvedValueOnce(mockDocument);
- const { result, waitForNextUpdate, rerender } = renderHook(
+ const { result, rerender } = renderHook(
({ selectedPersona }) => {
mockUseApplicationStore.mockReturnValue({
selectedPersona,
@@ -149,16 +180,17 @@ describe('useCustomPages', () => {
initialProps: {
selectedPersona: { fullyQualifiedName: 'test-persona' },
},
+ wrapper: createWrapper(queryClient),
}
);
- await waitForNextUpdate();
+ await waitFor(() => {
+ expect(result.current.customizedPage).toEqual(mockDocument.data.pages[0]);
+ });
expect(mockGetDocumentByFQN).toHaveBeenCalledWith('persona.test-persona');
- expect(result.current.customizedPage).toEqual(mockDocument.data.pages[0]);
expect(result.current.navigation).toEqual(mockDocument.data.navigation);
- // Change the selected persona
const newPersona = { fullyQualifiedName: 'new-persona' };
mockGetDocumentByFQN.mockResolvedValueOnce({
entityType: 'PERSONA',
@@ -172,13 +204,14 @@ describe('useCustomPages', () => {
rerender({ selectedPersona: newPersona });
- await waitForNextUpdate();
+ await waitFor(() => {
+ expect(result.current.customizedPage).toEqual({
+ pageType: PageType.Table,
+ content: 'New Content',
+ });
+ });
expect(mockGetDocumentByFQN).toHaveBeenCalledWith('persona.new-persona');
- expect(result.current.customizedPage).toEqual({
- pageType: PageType.Table,
- content: 'New Content',
- });
expect(result.current.navigation).toEqual([{ name: 'New Navigation' }]);
});
});
diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomPages.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomPages.ts
index 97324011fa38..9ac91b98fc88 100644
--- a/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomPages.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useCustomPages.ts
@@ -10,48 +10,52 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { useCallback, useEffect, useState } from 'react';
-import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
-import { EntityType } from '../enums/entity.enum';
+import { useQuery } from '@tanstack/react-query';
+import { useEffect, useState } from 'react';
import { Page, PageType } from '../generated/system/ui/page';
import { NavigationItem } from '../generated/system/ui/uiCustomization';
-import { getDocumentByFQN } from '../rest/DocStoreAPI';
+import {
+ docStoreQueryFn,
+ docStoreQueryKey,
+ personaDocFqn,
+ PERSONA_DOC_STALE_TIME,
+} from '../rest/queries/docStoreQuery';
import { useApplicationStore } from './useApplicationStore';
export const useCustomPages = (pageType: PageType | 'Navigation') => {
const { selectedPersona } = useApplicationStore();
- const [customizedPage, setCustomizedPage] = useState(null);
- const [navigation, setNavigation] = useState(null);
- const [isLoading, setIsLoading] = useState(true);
+ const fqn = personaDocFqn(selectedPersona);
- const fetchDocument = useCallback(async () => {
- const pageFQN = `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${selectedPersona?.fullyQualifiedName}`;
- try {
- const doc = await getDocumentByFQN(pageFQN);
- setCustomizedPage(
- doc.data?.pages?.find((p: Page | null) => p?.pageType === pageType)
- );
- setNavigation(doc.data?.navigation);
- } catch (error) {
- // Need to reset Navigation to avoid showing old navigation items
- setNavigation([]);
- setCustomizedPage(null);
- } finally {
- setIsLoading(false);
- }
- }, [selectedPersona?.fullyQualifiedName, pageType]);
+ const { data: doc, isError } = useQuery({
+ queryKey: docStoreQueryKey(fqn ?? ''),
+ queryFn: docStoreQueryFn(fqn ?? ''),
+ enabled: !!fqn,
+ retry: false,
+ staleTime: PERSONA_DOC_STALE_TIME,
+ });
+ // hasMounted flips once after the first paint so entity pages always show
+ // their loader on first render — identical to the old useState(true) pattern.
+ // Without this, selectedPersona arrives asynchronously after first render,
+ // causing isLoading to flip false→true→false in a window where
+ // waitForAllLoadersToDisappear may have already returned.
+ const [hasMounted, setHasMounted] = useState(false);
useEffect(() => {
- if (selectedPersona?.fullyQualifiedName) {
- fetchDocument();
- } else {
- setIsLoading(false);
- }
- }, [selectedPersona, pageType]);
+ setHasMounted(true);
+ }, []);
return {
- customizedPage,
- navigation,
- isLoading,
+ customizedPage:
+ (doc?.data?.pages?.find((p: Page | null) => p?.pageType === pageType) as
+ | Page
+ | undefined) ?? null,
+ // Reset to [] on error to clear stale navigation items, null when no persona selected.
+ navigation: isError
+ ? ([] as NavigationItem[])
+ : ((doc?.data?.navigation ?? null) as NavigationItem[] | null),
+ // Only block render on the very first paint. The persona doc fetches in the
+ // background after that; customizedPage/navigation update when it arrives
+ // without re-showing a loader (matches the old fetchDocument behaviour).
+ isLoading: !hasMounted,
};
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.component.tsx
index 95d7d0cf995f..ae94358ae15a 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.component.tsx
@@ -11,6 +11,7 @@
* limitations under the License.
*/
+import { useQuery } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isEmpty } from 'lodash';
@@ -25,7 +26,6 @@ import CustomiseLandingPageHeader from '../../components/MyData/CustomizableComp
import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1';
import { LOGGED_IN_USER_STORAGE_KEY } from '../../constants/constants';
import { LandingPageWidgetKeys } from '../../enums/CustomizablePage.enum';
-import { EntityType } from '../../enums/entity.enum';
import type { Page } from '../../generated/system/ui/page';
import { PageType } from '../../generated/system/ui/page';
import type { PersonaPreferences } from '../../generated/type/personaPreferences';
@@ -37,7 +37,12 @@ import {
AnnouncementEntity,
getActiveAnnouncements,
} from '../../rest/announcementsAPI';
-import { getDocumentByFQN } from '../../rest/DocStoreAPI';
+import {
+ docStoreQueryFn,
+ docStoreQueryKey,
+ personaDocFqn,
+ PERSONA_DOC_STALE_TIME,
+} from '../../rest/queries/docStoreQuery';
import { updateUserDetail } from '../../rest/userAPI';
import { getConstrainedWidgetWidth } from '../../utils/CustomizableLandingPagePureUtils';
import customizeMyDataPageClassBase from '../../utils/CustomizeMyDataPageClassBase';
@@ -75,18 +80,59 @@ const MyDataPage = () => {
useApplicationStore();
const { isWelcomeVisible } = useWelcomeStore();
- const [isLoading, setIsLoading] = useState(true);
- const [layout, setLayout] = useState>(
- getDefaultLandingPageLayout
- );
-
const [showWelcomeScreen, setShowWelcomeScreen] = useState(false);
const [isAnnouncementLoading, setIsAnnouncementLoading] =
useState(true);
const [announcements, setAnnouncements] = useState([]);
- const [personaPreferences, setPersonaPreferences] = useState<
- PersonaPreferences[]
- >([]);
+
+ const personaFqn = personaDocFqn(selectedPersona);
+
+ const { data: docData, isPending: isDocPending } = useQuery({
+ queryKey: docStoreQueryKey(personaFqn ?? ''),
+ queryFn: docStoreQueryFn(personaFqn ?? ''),
+ enabled: !!personaFqn,
+ retry: false,
+ staleTime: PERSONA_DOC_STALE_TIME,
+ });
+
+ // hasMounted flips once after the first paint so the skeleton always shows on
+ // first render, deferring widget loaders until after that paint. Without this
+ // guard a user with no persona gets isLoading=false immediately, exposing
+ // widget loaders to Playwright's waitForAllLoadersToDisappear too early.
+ const [hasMounted, setHasMounted] = useState(false);
+ useEffect(() => {
+ setHasMounted(true);
+ }, []);
+ const isLoading = !hasMounted || (!!personaFqn && isDocPending);
+
+ const personaPreferences = useMemo(
+ () => docData?.data?.personPreferences ?? [],
+ [docData]
+ );
+
+ const layout = useMemo>(() => {
+ if (!docData || !selectedPersona) {
+ return getDefaultLandingPageLayout();
+ }
+ const pageData = docData.data?.pages?.find(
+ (p: Page) => p.pageType === PageType.LandingPage
+ ) ?? { layout: [], pageType: PageType.LandingPage };
+ const filteredLayout = (pageData.layout as WidgetConfig[])
+ .filter(
+ (widget: WidgetConfig) =>
+ !widget.i.startsWith(LandingPageWidgetKeys.CURATED_ASSETS) ||
+ !isEmpty(widget.config)
+ )
+ .map((widget: WidgetConfig) => ({
+ ...widget,
+ w: getConstrainedWidgetWidth(widget.w),
+ h: 3,
+ }));
+
+ return isEmpty(filteredLayout)
+ ? getDefaultLandingPageLayout()
+ : filteredLayout;
+ }, [docData, selectedPersona]);
const storageData = useMemo(
() => localStorage.getItem(LOGGED_IN_USER_STORAGE_KEY),
[]
@@ -118,49 +164,6 @@ const MyDataPage = () => {
return userPersonaBackgroundColor ?? adminPersonaBackgroundColor;
}, [userPersonaBackgroundColor, adminPersonaBackgroundColor]);
- const fetchDocument = async () => {
- setIsLoading(true);
-
- try {
- if (selectedPersona) {
- const pageFQN = `${EntityType.PERSONA}.${selectedPersona.fullyQualifiedName}`;
- const docData = await getDocumentByFQN(pageFQN);
-
- setPersonaPreferences(docData.data?.personPreferences ?? []);
-
- const pageData = docData.data?.pages?.find(
- (p: Page) => p.pageType === PageType.LandingPage
- ) ?? { layout: [], pageType: PageType.LandingPage };
-
- const filteredLayout = pageData.layout
- .filter(
- (widget: WidgetConfig) =>
- !widget.i.startsWith(LandingPageWidgetKeys.CURATED_ASSETS) ||
- !isEmpty(widget.config)
- )
- .map((widget: WidgetConfig) => {
- return {
- ...widget,
- w: getConstrainedWidgetWidth(widget.w),
- h: 3,
- };
- });
-
- setLayout(
- isEmpty(filteredLayout)
- ? getDefaultLandingPageLayout()
- : filteredLayout
- );
- } else {
- setLayout(getDefaultLandingPageLayout());
- }
- } catch {
- setLayout(getDefaultLandingPageLayout());
- } finally {
- setIsLoading(false);
- }
- };
-
const updateWelcomeScreen = (show: boolean) => {
if (loggedInUserName) {
const arr = storageData ? storageData.split(',') : [];
@@ -172,10 +175,6 @@ const MyDataPage = () => {
setShowWelcomeScreen(show);
};
- useEffect(() => {
- fetchDocument();
- }, [selectedPersona]);
-
useEffect(() => {
updateWelcomeScreen(!usernameExistsInCookie && isWelcomeVisible);
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.test.tsx
index 410f13e0d4bb..656ec80e2c36 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/MyDataPage/MyDataPage.test.tsx
@@ -10,6 +10,7 @@
* 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, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { PageType } from '../../generated/system/ui/page';
@@ -213,8 +214,20 @@ jest.mock(
})
);
+let queryClient: QueryClient;
+
+const renderMyDataPage = () =>
+ render(
+
+
+
+ );
+
describe('MyDataPage component', () => {
beforeEach(() => {
+ queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
localStorage.setItem('loggedInUsers', mockUserData.name);
mockSelectedPersona = {
fullyQualifiedName: mockPersonaName,
@@ -227,7 +240,7 @@ describe('MyDataPage component', () => {
// Simulate no user is logged in condition
localStorage.clear();
- render();
+ renderMyDataPage();
expect(await screen.findByText('WelcomeScreen')).toBeInTheDocument();
});
@@ -236,7 +249,7 @@ describe('MyDataPage component', () => {
// Simulate no user is logged in condition
localStorage.clear();
- render();
+ renderMyDataPage();
const welcomeScreen = await screen.findByText('WelcomeScreen');
@@ -249,7 +262,7 @@ describe('MyDataPage component', () => {
});
it('MyDataPage should display skeleton while resolving the landing page layout', async () => {
- render();
+ renderMyDataPage();
expect(screen.getByText('MyDataPageSkeleton')).toBeInTheDocument();
expect(screen.queryByTestId('react-grid-layout')).not.toBeInTheDocument();
@@ -260,7 +273,7 @@ describe('MyDataPage component', () => {
});
it('MyDataPage should render CustomiseLandingPageHeader component', async () => {
- render();
+ renderMyDataPage();
expect(
screen.getByTestId('customise-landing-page-header')
@@ -269,7 +282,7 @@ describe('MyDataPage component', () => {
});
it('MyDataPage should display all the widgets in the config and the announcements widget if there are announcements', async () => {
- render();
+ renderMyDataPage();
expect(
await screen.findByText('KnowledgePanel.ActivityFeed')
@@ -295,7 +308,7 @@ describe('MyDataPage component', () => {
data: [],
})
);
- render();
+ renderMyDataPage();
expect(
await screen.findByText('KnowledgePanel.ActivityFeed')
@@ -318,7 +331,7 @@ describe('MyDataPage component', () => {
(getDocumentByFQN as jest.Mock).mockImplementationOnce(() =>
Promise.reject(new Error('API failure'))
);
- render();
+ renderMyDataPage();
expect(
await screen.findByText('KnowledgePanel.ActivityFeed')
@@ -344,7 +357,7 @@ describe('MyDataPage component', () => {
it('MyDataPage should render default widgets when there is no selected persona', async () => {
mockSelectedPersona = null;
await act(async () => {
- render();
+ renderMyDataPage();
});
await screen.findByTestId('page-layout-v1');
@@ -367,7 +380,7 @@ describe('MyDataPage component', () => {
describe('Component Structure', () => {
it('should render the correct page structure with grid wrapper', async () => {
await act(async () => {
- render();
+ renderMyDataPage();
});
expect(screen.getByTestId('page-layout-v1')).toBeInTheDocument();
@@ -380,7 +393,7 @@ describe('MyDataPage component', () => {
it('should render CustomiseLandingPageHeader before the grid layout', async () => {
await act(async () => {
- render();
+ renderMyDataPage();
});
const pageLayout = screen.getByTestId('page-layout-v1');
@@ -396,7 +409,7 @@ describe('MyDataPage component', () => {
// Simulate no user is logged in condition
localStorage.clear();
await act(async () => {
- render();
+ renderMyDataPage();
});
expect(screen.getByText('WelcomeScreen')).toBeInTheDocument();
@@ -407,7 +420,7 @@ describe('MyDataPage component', () => {
it('should render the main content structure when not loading or showing welcome screen', async () => {
await act(async () => {
- render();
+ renderMyDataPage();
});
// Verify main content elements are present
diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/queries/docStoreQuery.ts b/openmetadata-ui/src/main/resources/ui/src/rest/queries/docStoreQuery.ts
new file mode 100644
index 000000000000..48a1ef3d63f0
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/rest/queries/docStoreQuery.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
+import { EntityType } from '../../enums/entity.enum';
+import { Document } from '../../generated/entity/docStore/document';
+import { getDocumentByFQN } from '../DocStoreAPI';
+
+/**
+ * Shared query plumbing for a single DocStore document by FQN. Any consumer
+ * that wants a cache-aware read — page customisation hooks, sidebar navigation,
+ * the My Data landing page — should go through {@link docStoreQueryKey} +
+ * {@link docStoreQueryFn} so they all hit the same normalised cache slot.
+ *
+ * React Query deduplicates in-flight requests: if multiple components mount
+ * simultaneously with the same FQN key (e.g. the sidebar navigation hook and
+ * the My Data page both reading `persona.X`), only one network request fires
+ * and both subscribers receive the result.
+ *
+ * {@link PERSONA_DOC_STALE_TIME} extends deduplication beyond concurrent
+ * mounts: staggered subscribers (sidebar renders a tick before page body)
+ * reuse the cached document instead of triggering a background refetch.
+ * Matches the staleTime used in useResolvedAppMode for the same endpoint.
+ */
+export const PERSONA_DOC_STALE_TIME = 5 * 60 * 1000;
+
+export const docStoreQueryKey = (fqn: string) => ['docStore', fqn] as const;
+
+export const docStoreQueryFn = (fqn: string) => (): Promise =>
+ getDocumentByFQN(fqn);
+
+/**
+ * Derive the docStore FQN for a persona's UICustomization document.
+ * Returns null when the persona has no FQN (disabled query guard).
+ */
+export const personaDocFqn = (
+ persona?: {
+ fullyQualifiedName?: string;
+ } | null
+): string | null =>
+ persona?.fullyQualifiedName
+ ? `${EntityType.PERSONA}${FQN_SEPARATOR_CHAR}${persona.fullyQualifiedName}`
+ : null;