Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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 { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook } from '@testing-library/react-hooks';
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 @@ -61,6 +72,11 @@ describe('useCustomPages', () => {

beforeEach(() => {
jest.clearAllMocks();
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
mockUseApplicationStore.mockReturnValue({
selectedPersona: mockSelectedPersona,
});
Expand All @@ -69,8 +85,9 @@ 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)
const { result, waitForNextUpdate } = renderHook(
() => useCustomPages(PageType.Table),
{ wrapper: createWrapper(queryClient) }
);

expect(result.current.isLoading).toBe(true);
Expand All @@ -86,8 +103,9 @@ describe('useCustomPages', () => {
it('should handle error when fetching document fails', async () => {
mockGetDocumentByFQN.mockRejectedValue(new Error('API Error'));

const { result, waitForNextUpdate } = renderHook(() =>
useCustomPages(PageType.Table)
const { result, waitForNextUpdate } = renderHook(
() => useCustomPages(PageType.Table),
{ wrapper: createWrapper(queryClient) }
);

expect(result.current.isLoading).toBe(true);
Expand All @@ -105,33 +123,47 @@ describe('useCustomPages', () => {
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);
});

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, waitForNextUpdate } = renderHook(
({ pageType }: { pageType: PageType }) => useCustomPages(pageType),
{
initialProps: { pageType: PageType.Table },
wrapper: createWrapper(queryClient),
}
);

await waitForNextUpdate();

expect(mockGetDocumentByFQN).toHaveBeenCalledTimes(1);
expect(result.current.customizedPage?.pageType).toBe(PageType.Table);

rerender({ pageType: PageType.Dashboard });

await waitForNextUpdate();

expect(mockGetDocumentByFQN).toHaveBeenCalledTimes(2);
// Changing pageType filters from the cached doc — no additional network request.
expect(mockGetDocumentByFQN).toHaveBeenCalledTimes(1);
expect(result.current.customizedPage?.pageType).toBe(PageType.Dashboard);
});

it('should return updated results when selected persona changes', async () => {
Expand All @@ -149,6 +181,7 @@ describe('useCustomPages', () => {
initialProps: {
selectedPersona: { fullyQualifiedName: 'test-persona' },
},
wrapper: createWrapper(queryClient),
}
);

Expand Down
60 changes: 26 additions & 34 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,40 @@
* 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 { Page, PageType } from '../generated/system/ui/page';
import { NavigationItem } from '../generated/system/ui/uiCustomization';
import { getDocumentByFQN } from '../rest/DocStoreAPI';
import {
docStoreQueryFn,
docStoreQueryKey,
personaDocFqn,
} 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]);

useEffect(() => {
if (selectedPersona?.fullyQualifiedName) {
fetchDocument();
} else {
setIsLoading(false);
}
}, [selectedPersona, pageType]);
const {
data: doc,
isPending,
isError,
} = useQuery({
queryKey: docStoreQueryKey(fqn ?? ''),
queryFn: docStoreQueryFn(fqn ?? ''),
enabled: !!fqn,
retry: false,
});

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),
isLoading: !!fqn && isPending,
};
};
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 { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isEmpty } from 'lodash';
Expand All @@ -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';
Expand All @@ -37,7 +37,11 @@ import {
AnnouncementEntity,
getActiveAnnouncements,
} from '../../rest/announcementsAPI';
import { getDocumentByFQN } from '../../rest/DocStoreAPI';
import {
docStoreQueryFn,
docStoreQueryKey,
personaDocFqn,
} from '../../rest/queries/docStoreQuery';
import { updateUserDetail } from '../../rest/userAPI';
import { getConstrainedWidgetWidth } from '../../utils/CustomizableLandingPagePureUtils';
import customizeMyDataPageClassBase from '../../utils/CustomizeMyDataPageClassBase';
Expand Down Expand Up @@ -75,18 +79,50 @@ const MyDataPage = () => {
useApplicationStore();
const { isWelcomeVisible } = useWelcomeStore();

const [isLoading, setIsLoading] = useState(true);
const [layout, setLayout] = useState<Array<WidgetConfig>>(
getDefaultLandingPageLayout
);

const [showWelcomeScreen, setShowWelcomeScreen] = useState(false);
const [isAnnouncementLoading, setIsAnnouncementLoading] =
useState<boolean>(true);
const [announcements, setAnnouncements] = useState<AnnouncementEntity[]>([]);
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,
});

const isLoading = !!personaFqn && isDocPending;

const personaPreferences = useMemo<PersonaPreferences[]>(
() => docData?.data?.personPreferences ?? [],
[docData]
);

const layout = useMemo<Array<WidgetConfig>>(() => {
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 = localStorage.getItem(LOGGED_IN_USER_STORAGE_KEY);

const loggedInUserName = useMemo(() => {
Expand Down Expand Up @@ -115,49 +151,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(',') : [];
Expand All @@ -169,10 +162,6 @@ const MyDataPage = () => {
setShowWelcomeScreen(show);
};

useEffect(() => {
fetchDocument();
}, [selectedPersona]);

useEffect(() => {
updateWelcomeScreen(!usernameExistsInCookie && isWelcomeVisible);

Expand Down
Loading
Loading