diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/EntityPopOverCard.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/EntityPopOverCard.test.tsx new file mode 100644 index 00000000000..63009baae89 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/EntityPopOverCard.test.tsx @@ -0,0 +1,236 @@ +/* + * Copyright 2024 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 { act, render, screen } from '@testing-library/react'; +import React from 'react'; +import { EntityType } from '../../../enums/entity.enum'; +import { MOCK_TAG_DATA, MOCK_TAG_ENCODED_FQN } from '../../../mocks/Tags.mock'; +import { getTagByFqn } from '../../../rest/tagAPI'; +import { useApplicationConfigContext } from '../../ApplicationConfigProvider/ApplicationConfigProvider'; +import EntityPopOverCard, { PopoverContent } from './EntityPopOverCard'; + +const updateCachedEntityData = jest.fn(); + +jest.mock('../../../utils/CommonUtils', () => ({ + getTableFQNFromColumnFQN: jest.fn(), +})); + +jest.mock('../../../utils/EntityUtils', () => ({ + getEntityName: jest.fn(), +})); + +jest.mock('../../../utils/StringsUtils', () => ({ + getDecodedFqn: jest.fn(), + getEncodedFqn: jest.fn(), +})); + +jest.mock('../../Loader/Loader', () => { + return jest.fn().mockImplementation(() =>

Loader

); +}); + +jest.mock('../../ExploreV1/ExploreSearchCard/ExploreSearchCard', () => { + return jest.fn().mockImplementation(() =>

ExploreSearchCard

); +}); + +jest.mock('../../../rest/dashboardAPI', () => ({ + getDashboardByFqn: jest.fn().mockImplementation(() => Promise.resolve({})), +})); + +jest.mock('../../../rest/dataModelsAPI', () => ({ + getDataModelDetailsByFQN: jest + .fn() + .mockImplementation(() => Promise.resolve({})), +})); + +jest.mock('../../../rest/dataProductAPI', () => ({ + getDataProductByName: jest.fn().mockImplementation(() => Promise.resolve({})), +})); + +jest.mock('../../../rest/databaseAPI', () => ({ + getDatabaseDetailsByFQN: jest + .fn() + .mockImplementation(() => Promise.resolve({})), + getDatabaseSchemaDetailsByFQN: jest + .fn() + .mockImplementation(() => Promise.resolve({})), +})); + +jest.mock('../../../rest/domainAPI', () => ({ + getDomainByName: jest.fn().mockImplementation(() => Promise.resolve({})), +})); + +jest.mock('../../../rest/glossaryAPI', () => ({ + getGlossariesByName: jest.fn().mockImplementation(() => Promise.resolve({})), + getGlossaryTermByFQN: jest.fn().mockImplementation(() => Promise.resolve({})), +})); + +jest.mock('../../../rest/mlModelAPI', () => ({ + getMlModelByFQN: jest.fn().mockImplementation(() => Promise.resolve({})), +})); + +jest.mock('../../../rest/pipelineAPI', () => ({ + getPipelineByFqn: jest.fn().mockImplementation(() => Promise.resolve({})), +})); + +jest.mock('../../../rest/storageAPI', () => ({ + getContainerByFQN: jest.fn().mockImplementation(() => Promise.resolve({})), +})); + +jest.mock('../../../rest/storedProceduresAPI', () => ({ + getStoredProceduresDetailsByFQN: jest + .fn() + .mockImplementation(() => Promise.resolve({})), +})); + +jest.mock('../../../rest/tableAPI', () => ({ + getTableDetailsByFQN: jest.fn().mockImplementation(() => Promise.resolve({})), +})); + +jest.mock('../../../rest/tagAPI', () => ({ + getTagByFqn: jest.fn().mockImplementation(() => Promise.resolve({})), +})); + +jest.mock('../../../rest/topicsAPI', () => ({ + getTopicByFqn: jest.fn().mockImplementation(() => Promise.resolve({})), +})); + +jest.mock('../../ApplicationConfigProvider/ApplicationConfigProvider', () => ({ + useApplicationConfigContext: jest.fn().mockImplementation(() => ({ + cachedEntityData: {}, + updateCachedEntityData, + })), +})); + +describe('Test EntityPopoverCard component', () => { + it('EntityPopoverCard should render element', () => { + render( + +
Test_Popover
+
+ ); + + expect(screen.getByTestId('popover-container')).toBeInTheDocument(); + }); + + it('EntityPopoverCard should render loader on initial render', async () => { + render( + + ); + + const loader = screen.getByText('Loader'); + + expect(loader).toBeInTheDocument(); + }); + + it("EntityPopoverCard should show no data placeholder if entity type doesn't match", async () => { + await act(async () => { + render( + + ); + }); + + expect(screen.getByText('label.no-data-found')).toBeInTheDocument(); + }); + + it('EntityPopoverCard should show no data placeholder if api call fail', async () => { + (getTagByFqn as jest.Mock).mockImplementationOnce(() => + Promise.reject({ + response: { + data: { message: 'Error!' }, + }, + }) + ); + + await act(async () => { + render( + + ); + }); + + expect(screen.getByText('label.no-data-found')).toBeInTheDocument(); + }); + + it('EntityPopoverCard should call tags api if entity type is tag card', async () => { + const mockTagAPI = getTagByFqn as jest.Mock; + + await act(async () => { + render( + + ); + }); + + expect(mockTagAPI.mock.calls[0][0]).toBe(MOCK_TAG_ENCODED_FQN); + }); + + it('EntityPopoverCard should call api and trigger updateCachedEntityData in provider', async () => { + const mockTagAPI = getTagByFqn as jest.Mock; + + (getTagByFqn as jest.Mock).mockImplementationOnce(() => + Promise.resolve(MOCK_TAG_DATA) + ); + + await act(async () => { + render( + + ); + }); + + expect(mockTagAPI.mock.calls[0][0]).toBe(MOCK_TAG_ENCODED_FQN); + + expect(updateCachedEntityData).toHaveBeenCalledWith({ + id: MOCK_TAG_ENCODED_FQN, + entityDetails: MOCK_TAG_DATA, + }); + }); + + it('EntityPopoverCard should not call api if cached data is available', async () => { + const mockTagAPI = getTagByFqn as jest.Mock; + + (useApplicationConfigContext as jest.Mock).mockImplementation(() => ({ + cachedEntityData: { + [MOCK_TAG_ENCODED_FQN]: { + name: 'test', + }, + }, + })); + + await act(async () => { + render( + + ); + }); + + expect(mockTagAPI.mock.calls).toEqual([]); + expect(screen.getByText('ExploreSearchCard')).toBeInTheDocument(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/EntityPopOverCard.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/EntityPopOverCard.tsx index f9b14ec9f23..28fe2d9dce3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/EntityPopOverCard.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/PopOverCard/EntityPopOverCard.tsx @@ -11,7 +11,8 @@ * limitations under the License. */ -import { Popover } from 'antd'; +import { Popover, Typography } from 'antd'; +import { isUndefined } from 'lodash'; import React, { FC, HTMLAttributes, @@ -20,6 +21,7 @@ import React, { useMemo, useState, } from 'react'; +import { useTranslation } from 'react-i18next'; import { EntityType } from '../../../enums/entity.enum'; import { Table } from '../../../generated/entity/data/table'; import { Include } from '../../../generated/type/include'; @@ -40,6 +42,7 @@ import { getPipelineByFqn } from '../../../rest/pipelineAPI'; import { getContainerByFQN } from '../../../rest/storageAPI'; import { getStoredProceduresDetailsByFQN } from '../../../rest/storedProceduresAPI'; import { getTableDetailsByFQN } from '../../../rest/tableAPI'; +import { getTagByFqn } from '../../../rest/tagAPI'; import { getTopicByFqn } from '../../../rest/topicsAPI'; import { getTableFQNFromColumnFQN } from '../../../utils/CommonUtils'; import { getEntityName } from '../../../utils/EntityUtils'; @@ -48,6 +51,7 @@ import { useApplicationConfigContext } from '../../ApplicationConfigProvider/App import { EntityUnion } from '../../Explore/ExplorePage.interface'; import ExploreSearchCard from '../../ExploreV1/ExploreSearchCard/ExploreSearchCard'; import Loader from '../../Loader/Loader'; +import { SearchedDataProps } from '../../SearchedData/SearchedData.interface'; import './popover-card.less'; interface Props extends HTMLAttributes { @@ -55,17 +59,33 @@ interface Props extends HTMLAttributes { entityFQN: string; } -const PopoverContent: React.FC<{ +export const PopoverContent: React.FC<{ entityFQN: string; entityType: string; }> = ({ entityFQN, entityType }) => { + const { t } = useTranslation(); const [loading, setLoading] = useState(true); const { cachedEntityData, updateCachedEntityData } = useApplicationConfigContext(); - const entityData = useMemo( - () => cachedEntityData[entityFQN], - [cachedEntityData, entityFQN] - ); + + const entityData: SearchedDataProps['data'][number]['_source'] | undefined = + useMemo(() => { + const data = cachedEntityData[entityFQN]; + + return data + ? { + ...data, + name: data.name, + displayName: getEntityName(data), + id: data.id ?? '', + description: data.description ?? '', + fullyQualifiedName: getDecodedFqn(entityFQN), + tags: (data as Table)?.tags, + entityType: entityType, + serviceType: (data as Table)?.serviceType, + } + : data; + }, [cachedEntityData, entityFQN]); const getData = useCallback(async () => { const fields = 'tags,owner'; @@ -146,6 +166,11 @@ const PopoverContent: React.FC<{ break; + case EntityType.TAG: + promise = getTagByFqn(entityFQN); + + break; + default: break; } @@ -162,11 +187,14 @@ const PopoverContent: React.FC<{ } else { setLoading(false); } - }, [entityType, entityFQN]); + }, [entityType, entityFQN, updateCachedEntityData]); useEffect(() => { const entityData = cachedEntityData[entityFQN]; - if (!entityData) { + + if (entityData) { + setLoading(false); + } else { getData(); } }, [entityFQN]); @@ -175,21 +203,15 @@ const PopoverContent: React.FC<{ return ; } + if (isUndefined(entityData)) { + return {t('label.no-data-found')}; + } + return ( ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/mocks/Tags.mock.ts b/openmetadata-ui/src/main/resources/ui/src/mocks/Tags.mock.ts index d053dc19d85..93555a0c10b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/mocks/Tags.mock.ts +++ b/openmetadata-ui/src/main/resources/ui/src/mocks/Tags.mock.ts @@ -11,6 +11,8 @@ * limitations under the License. */ +export const MOCK_TAG_ENCODED_FQN = '"%22Mock.Tag%22.Tag_1"'; + export const mockTagList = [ { id: 'e649c601-44d3-449d-bc04-fbbaf83baf19', @@ -26,3 +28,30 @@ export const mockTagList = [ mutuallyExclusive: true, }, ]; + +export const MOCK_TAG_DATA = { + id: 'e8bc85c8-a87f-471c-872e-46904c5ea888', + name: 'search_part_2', + displayName: '', + fullyQualifiedName: 'advanceSearch.search_part_2', + description: 'this is search_part_2', + style: {}, + classification: { + id: '16c5865a-8804-4474-a1dd-14ee9da443b2', + type: 'classification', + name: 'advanceSearch', + fullyQualifiedName: 'advanceSearch', + description: 'advanceSearch', + displayName: '', + deleted: false, + href: 'http://sandbox-beta.open-metadata.org/api/v1/classifications/16c5865a-8804-4474-a1dd-14ee9da443b2', + }, + version: 0.1, + updatedAt: 1704261482857, + updatedBy: 'ashish', + href: 'http://sandbox-beta.open-metadata.org/api/v1/tags/e8bc85c8-a87f-471c-872e-46904c5ea888', + deprecated: false, + deleted: false, + provider: 'user', + mutuallyExclusive: false, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/tagAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/tagAPI.ts index c368a3bdc27..cd7bd0ac4e7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/tagAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/tagAPI.ts @@ -19,6 +19,7 @@ import { CreateTag } from '../generated/api/classification/createTag'; import { Classification } from '../generated/entity/classification/classification'; import { Tag } from '../generated/entity/classification/tag'; import { EntityHistory } from '../generated/type/entityHistory'; +import { ListParams } from '../interface/API.interface'; import { getURLWithQueryFields } from '../utils/APIUtils'; import APIClient from './index'; @@ -108,6 +109,14 @@ export const patchClassification = async (id: string, data: Operation[]) => { return response.data; }; +export const getTagByFqn = async (fqn: string, params?: ListParams) => { + const response = await APIClient.get(`tags/name/${fqn}`, { + params, + }); + + return response.data; +}; + export const createTag = async (data: CreateTag) => { const response = await APIClient.post>( `/tags`, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/APIUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/APIUtils.ts index 88093d1f110..4f620b656ef 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/APIUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/APIUtils.ts @@ -191,6 +191,9 @@ export const formatSearchTagsResponse = ( })); }; +/** + * @deprecated getURLWithQueryFields is deprecated, Please use params to pass query parameters wherever it is required + */ export const getURLWithQueryFields = ( url: string, lstQueryFields?: string | string[],