Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d5cebde
perf(ui): deduplicate docStore requests via React Query shared key
Rohit0301 Aug 10, 2026
fc5245d
lint fix
Rohit0301 Aug 10, 2026
dcb5759
refactor(ui): centralize persona FQN construction in personaDocFqn he…
Rohit0301 Aug 10, 2026
efbb78b
fix(test): restore 5 failing tests after React Query migration
Rohit0301 Aug 10, 2026
960fc5c
lint fix
Rohit0301 Aug 10, 2026
3255aba
perf(ui): deduplicate docStore requests via React Query shared key
Rohit0301 Aug 12, 2026
3d283ca
revert: restore useCustomPages and Glossary tests to original state
Rohit0301 Aug 12, 2026
98d2104
perf(ui): deduplicate docStore requests via React Query in useCustomP…
Rohit0301 Aug 12, 2026
fce2832
Merge remote-tracking branch 'origin/main' into duplicate-persona-onb…
Rohit0301 Aug 12, 2026
7610a3b
perf(ui): add staleTime to docStore queries to cover staggered mounts
Rohit0301 Aug 12, 2026
b31aa55
lint fix
Rohit0301 Aug 12, 2026
5d921c7
fixed unit test
Rohit0301 Aug 13, 2026
5d101dd
Merge branch 'main' into duplicate-persona-onboarding-requests
Rohit0301 Aug 13, 2026
62057ea
Merge branch 'main' into duplicate-persona-onboarding-requests
Rohit0301 Aug 14, 2026
b34265d
fix(ui): restore first-render skeleton in MyDataPage to prevent Playw…
Rohit0301 Aug 15, 2026
25ed7c0
refactor(ui): replace derived-state anti-pattern with hasMounted flag
Rohit0301 Aug 15, 2026
60c7daf
fix(ui): restore first-render skeleton in useCustomPages to prevent P…
Rohit0301 Aug 15, 2026
071670f
refactor(ui): replace derived-state anti-pattern with hasMounted flag
Rohit0301 Aug 15, 2026
9b35ec9
fix(ui): drop unused isPending from useCustomPages destructure
Rohit0301 Aug 15, 2026
a2aa987
lint fix
Rohit0301 Aug 15, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
import GlossaryDetails from './GlossaryDetails.component';

jest.mock('../GlossaryTermTab/GlossaryTermTab.component', () => {
return jest.fn().mockReturnValue(<p>GlossaryTermTab.component</p>);

Check warning on line 24 in openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.test.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

disallow literal string: <p>GlossaryTermTab.component</p>
});
jest.mock('../GlossaryHeader/GlossaryHeader.component', () => {
return jest.fn().mockReturnValue(<p>GlossaryHeader.component</p>);

Check warning on line 27 in openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.test.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

disallow literal string: <p>GlossaryHeader.component</p>
});
jest.mock('react-router-dom', () => ({
Link: jest
Expand All @@ -44,14 +44,22 @@
() => ({
ActivityFeedTab: jest
.fn()
.mockImplementation(() => <p>testActivityFeedTab</p>),

Check warning on line 47 in openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.test.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

disallow literal string: <p>testActivityFeedTab</p>
})
);

jest.mock('../../common/EntityDescription/Description', () =>
jest.fn().mockImplementation(() => <div>Description</div>)

Check warning on line 52 in openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.test.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

disallow literal string: <div>Description</div>
);

jest.mock('../../../hooks/useCustomPages', () => ({
useCustomPages: jest.fn().mockReturnValue({
customizedPage: null,
navigation: null,
isLoading: false,
}),
}));

const mockProps = {
glossary: mockedGlossaries[0],
glossaryTerms: [],
Expand Down Expand Up @@ -84,7 +92,7 @@
}));

jest.mock('../../Customization/GenericTab/GenericTab', () => ({
GenericTab: jest.fn().mockImplementation(() => <div>GenericTab</div>),

Check warning on line 95 in openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryDetails/GlossaryDetails.test.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

disallow literal string: <div>GenericTab</div>
}));

describe('Test Glossary-details component', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,25 +74,25 @@

jest.mock('react-router-dom', () => ({
useParams: jest.fn().mockImplementation(() => params),
Link: jest.fn().mockImplementation(({ children }) => <a>{children}</a>),

Check warning on line 77 in openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.test.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

The href attribute is required for an anchor to be keyboard accessible. Provide a valid, navigable address as the href value. If you cannot provide an href, but still need the element to resemble a link, use a button and change it with appropriate styles. Learn more: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/HEAD/docs/rules/anchor-is-valid.md
useNavigate: jest.fn().mockReturnValue(jest.fn()),
useLocation: jest.fn().mockImplementation(() => ({ pathname: 'mockPath' })),
}));

jest.mock('./GlossaryDetails/GlossaryDetails.component', () => {
return jest.fn().mockReturnValue(<>Glossary-Details component</>);

Check warning on line 83 in openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.test.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

disallow literal string: <>Glossary-Details component</>
});

jest.mock('./GlossaryTerms/GlossaryTermsV1.component', () => {
return jest.fn().mockReturnValue(<>Glossary-Term component</>);

Check warning on line 87 in openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.test.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

disallow literal string: <>Glossary-Term component</>
});

jest.mock('../common/TitleBreadcrumb/TitleBreadcrumb.component', () => {
return jest.fn().mockReturnValue(<>TitleBreadcrumb</>);

Check warning on line 91 in openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.test.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

disallow literal string: <>TitleBreadcrumb</>
});

jest.mock('../common/TitleBreadcrumb/TitleBreadcrumb.component', () =>
jest.fn().mockReturnValue(<div>Breadcrumb</div>)

Check warning on line 95 in openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.test.tsx

View workflow job for this annotation

GitHub Actions / checkstyle

disallow literal string: <div>Breadcrumb</div>
);

jest.mock('../common/ProfilePicture/ProfilePicture', () =>
Expand Down Expand Up @@ -130,6 +130,14 @@
})
);

jest.mock('../../hooks/useCustomPages', () => ({
useCustomPages: jest.fn().mockReturnValue({
customizedPage: null,
navigation: null,
isLoading: false,
}),
}));

const mockProps: GlossaryV1Props = {
selectedData: mockedGlossaries[0],
isGlossaryActive: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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',
};
Expand Down Expand Up @@ -60,6 +71,9 @@ describe('useCustomPages', () => {
};

beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
jest.clearAllMocks();
mockUseApplicationStore.mockReturnValue({
selectedPersona: mockSelectedPersona,
Expand All @@ -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,
Expand All @@ -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',
Expand All @@ -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' }]);
});
});
68 changes: 36 additions & 32 deletions openmetadata-ui/src/main/resources/ui/src/hooks/useCustomPages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Page | null>(null);
const [navigation, setNavigation] = useState<NavigationItem[] | null>(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,
};
};
Loading
Loading