From c6746507d90c05c1fb52320daf5f268b31c9f03f Mon Sep 17 00:00:00 2001 From: Sachin Chaurasiya Date: Fri, 24 Mar 2023 18:13:44 +0530 Subject: [PATCH] fix#10662: Update tier functionality is not working as expected on the containers page (#10720) * fix#10662: Update tier functionality is not working as expected on the containers page * chore: remove unwanted change * test: add unit test * chore: add container entity icon * chore: fix spacing issue * chore: add support for lineage info drawer * fix: locale missing key issue * refactor: entity info drawer --- .../EntityInfoDrawer.component.tsx | 207 ++++++--------- .../common/success-screen/SuccessScreen.tsx | 5 +- .../resources/ui/src/interface/types.d.ts | 8 + .../pages/ContainerPage/ContainerPage.mock.ts | 92 +++++++ .../ContainerPage/ContainerPage.test.tsx | 246 ++++++++++++++++++ .../src/pages/ContainerPage/ContainerPage.tsx | 7 +- .../resources/ui/src/utils/EntityUtils.tsx | 16 +- .../resources/ui/src/utils/TableUtils.tsx | 5 + 8 files changed, 449 insertions(+), 137 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.mock.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.test.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityInfoDrawer/EntityInfoDrawer.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityInfoDrawer/EntityInfoDrawer.component.tsx index 331deada318..d8c3dc8e276 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityInfoDrawer/EntityInfoDrawer.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityInfoDrawer/EntityInfoDrawer.component.tsx @@ -14,17 +14,21 @@ import { CloseOutlined } from '@ant-design/icons'; import { Col, Drawer, Row, Typography } from 'antd'; import classNames from 'classnames'; +import ContainerSummary from 'components/Explore/EntitySummaryPanel/ContainerSummary/ContainerSummary.component'; import DashboardSummary from 'components/Explore/EntitySummaryPanel/DashboardSummary/DashboardSummary.component'; import MlModelSummary from 'components/Explore/EntitySummaryPanel/MlModelSummary/MlModelSummary.component'; import PipelineSummary from 'components/Explore/EntitySummaryPanel/PipelineSummary/PipelineSummary.component'; import TableSummary from 'components/Explore/EntitySummaryPanel/TableSummary/TableSummary.component'; import TopicSummary from 'components/Explore/EntitySummaryPanel/TopicSummary/TopicSummary.component'; import { FQN_SEPARATOR_CHAR } from 'constants/char.constants'; +import { Container } from 'generated/entity/data/container'; import { Mlmodel } from 'generated/entity/data/mlmodel'; +import { EntityDetailUnion } from 'Models'; import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { getDashboardByFqn } from 'rest/dashboardAPI'; import { getMlModelByFQN } from 'rest/mlModelAPI'; +import { getContainerByName } from 'rest/objectStoreAPI'; import { getPipelineByFqn } from 'rest/pipelineAPI'; import { getTableDetailsByFQN } from 'rest/tableAPI'; import { getTopicByFqn } from 'rest/topicsAPI'; @@ -45,8 +49,6 @@ import { SelectedNode } from '../EntityLineage/EntityLineage.interface'; import { LineageDrawerProps } from './EntityInfoDrawer.interface'; import './EntityInfoDrawer.style.less'; -type EntityData = Table | Pipeline | Dashboard | Topic | Mlmodel; - const EntityInfoDrawer = ({ show, onCancel, @@ -54,136 +56,80 @@ const EntityInfoDrawer = ({ isMainNode = false, }: LineageDrawerProps) => { const { t } = useTranslation(); - const [entityDetail, setEntityDetail] = useState( - {} as EntityData + const [entityDetail, setEntityDetail] = useState( + {} as EntityDetailUnion ); const [isLoading, setIsLoading] = useState(false); - const fetchEntityDetail = (selectedNode: SelectedNode) => { - switch (selectedNode.type) { - case EntityType.TABLE: { - setIsLoading(true); - getTableDetailsByFQN(getEncodedFqn(selectedNode.fqn), [ - 'tags', - 'owner', - 'columns', - 'usageSummary', - 'profile', - ]) - .then((res) => { - setEntityDetail(res); - }) - .catch(() => { - showErrorToast( - t('server.error-selected-node-name-details', { - selectedNodeName: selectedNode.name, - }) - ); - }) - .finally(() => { - setIsLoading(false); - }); + const fetchEntityDetail = async (selectedNode: SelectedNode) => { + let response = {}; + const encodedFqn = getEncodedFqn(selectedNode.fqn); + const commonFields = ['tags', 'owner']; - break; + setIsLoading(true); + try { + switch (selectedNode.type) { + case EntityType.TABLE: { + response = await getTableDetailsByFQN(encodedFqn, [ + ...commonFields, + 'columns', + 'usageSummary', + 'profile', + ]); + + break; + } + case EntityType.PIPELINE: { + response = await getPipelineByFqn(encodedFqn, [ + ...commonFields, + 'followers', + 'tasks', + ]); + + break; + } + case EntityType.TOPIC: { + response = await getTopicByFqn(encodedFqn ?? '', commonFields); + + break; + } + case EntityType.DASHBOARD: { + response = await getDashboardByFqn(encodedFqn, [ + ...commonFields, + 'charts', + ]); + + break; + } + case EntityType.MLMODEL: { + response = await getMlModelByFQN(encodedFqn, [ + ...commonFields, + 'dashboard', + ]); + + break; + } + case EntityType.CONTAINER: { + response = await getContainerByName( + encodedFqn, + 'dataModel,owner,tags' + ); + + break; + } + default: + break; } - case EntityType.PIPELINE: { - setIsLoading(true); - getPipelineByFqn(getEncodedFqn(selectedNode.fqn), [ - 'tags', - 'owner', - 'followers', - 'tasks', - 'tier', - ]) - .then((res) => { - setEntityDetail(res); - setIsLoading(false); - }) - .catch(() => { - showErrorToast( - t('server.error-selected-node-name-details', { - selectedNodeName: selectedNode.name, - }) - ); - }) - .finally(() => { - setIsLoading(false); - }); - - break; - } - - case EntityType.TOPIC: { - setIsLoading(true); - getTopicByFqn(selectedNode.fqn ?? '', ['tags', 'owner']) - .then((res) => { - setEntityDetail(res); - }) - .catch(() => { - showErrorToast( - t('server.error-selected-node-name-details', { - selectedNodeName: selectedNode.name, - }) - ); - }) - .finally(() => { - setIsLoading(false); - }); - - break; - } - case EntityType.DASHBOARD: { - setIsLoading(true); - getDashboardByFqn(getEncodedFqn(selectedNode.fqn), [ - 'tags', - 'owner', - 'charts', - ]) - .then((res) => { - setEntityDetail(res); - setIsLoading(false); - }) - .catch(() => { - showErrorToast( - t('server.error-selected-node-name-details', { - selectedNodeName: selectedNode.name, - }) - ); - }) - .finally(() => { - setIsLoading(false); - }); - - break; - } - - case EntityType.MLMODEL: { - setIsLoading(true); - getMlModelByFQN(getEncodedFqn(selectedNode.fqn), [ - 'tags', - 'owner', - 'dashboard', - ]) - .then((res) => { - setEntityDetail(res); - setIsLoading(false); - }) - .catch(() => { - showErrorToast( - t('server.error-selected-node-name-details', { - selectedNodeName: selectedNode.name, - }) - ); - }) - .finally(() => { - setIsLoading(false); - }); - - break; - } - default: - break; + setEntityDetail(response); + } catch (error) { + showErrorToast( + t('server.error-selected-node-name-details', { + selectedNodeName: selectedNode.name, + }) + ); + } finally { + setIsLoading(false); } }; @@ -243,6 +189,15 @@ const EntityInfoDrawer = ({ tags={tags} /> ); + case EntityType.CONTAINER: + return ( + + ); default: return null; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/success-screen/SuccessScreen.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/success-screen/SuccessScreen.tsx index 2a31371fc9b..202e270ff59 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/success-screen/SuccessScreen.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/success-screen/SuccessScreen.tsx @@ -160,7 +160,10 @@ const SuccessScreen = ({ theme="primary" variant="outlined" onClick={handleViewServiceClick}> - {viewServiceText ?? 'View Service'} + + {viewServiceText ?? + t('label.view-entity', { entity: t('label.service') })} + {showIngestionButton && ( diff --git a/openmetadata-ui/src/main/resources/ui/src/interface/types.d.ts b/openmetadata-ui/src/main/resources/ui/src/interface/types.d.ts index 42d943f90bb..cd937f1d436 100644 --- a/openmetadata-ui/src/main/resources/ui/src/interface/types.d.ts +++ b/openmetadata-ui/src/main/resources/ui/src/interface/types.d.ts @@ -247,4 +247,12 @@ declare module 'Models' { } export type PagingWithoutTotal = Omit; + + type EntityDetailUnion = + | Table + | Pipeline + | Dashboard + | Topic + | Mlmodel + | Container; } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.mock.ts b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.mock.ts new file mode 100644 index 00000000000..18c800f2eb1 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.mock.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2023 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. + */ +export const CONTAINER_DATA = { + id: '5d11e32a-8673-4a84-a9be-ccd9651ba9fc', + name: 'transactions', + fullyQualifiedName: 's3_object_store_sample.transactions', + displayName: 'Company Transactions', + description: "Bucket containing all the company's transactions", + version: 0.7, + updatedAt: 1679567030351, + updatedBy: 'admin', + href: 'http://localhost:8585/api/v1/containers/5d11e32a-8673-4a84-a9be-ccd9651ba9fc', + owner: { + id: '28b43857-288b-4e4e-8fac-c9cd34e06393', + type: 'team', + name: 'Applications', + fullyQualifiedName: 'Applications', + deleted: false, + href: 'http://localhost:8585/api/v1/teams/28b43857-288b-4e4e-8fac-c9cd34e06393', + }, + service: { + id: 'cbc2a5e8-b7d3-4140-9a44-a4b331e5372f', + type: 'objectStoreService', + name: 's3_object_store_sample', + fullyQualifiedName: 's3_object_store_sample', + deleted: false, + href: 'http://localhost:8585/api/v1/services/objectstoreServices/cbc2a5e8-b7d3-4140-9a44-a4b331e5372f', + }, + dataModel: { + isPartitioned: true, + columns: [ + { + name: 'transaction_id', + dataType: 'NUMERIC', + dataTypeDisplay: 'numeric', + description: + 'The ID of the executed transaction. This column is the primary key for this table.', + fullyQualifiedName: + 's3_object_store_sample.transactions.transaction_id', + tags: [], + constraint: 'PRIMARY_KEY', + ordinalPosition: 1, + }, + { + name: 'merchant', + dataType: 'VARCHAR', + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'The merchant for this transaction.', + fullyQualifiedName: 's3_object_store_sample.transactions.merchant', + tags: [], + ordinalPosition: 2, + }, + { + name: 'transaction_time', + dataType: 'TIMESTAMP', + dataTypeDisplay: 'timestamp', + description: 'The time the transaction took place.', + fullyQualifiedName: + 's3_object_store_sample.transactions.transaction_time', + tags: [], + ordinalPosition: 3, + }, + ], + }, + prefix: '/transactions/', + numberOfObjects: 50, + size: 102400, + fileFormats: ['parquet'], + serviceType: 'S3', + followers: [], + tags: [ + { + tagFQN: 'Tier.Tier5', + description: '', + source: 'Classification', + labelType: 'Manual', + state: 'Confirmed', + }, + ], + deleted: false, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.test.tsx new file mode 100644 index 00000000000..4971542765a --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.test.tsx @@ -0,0 +1,246 @@ +/* + * Copyright 2023 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 { MemoryRouter } from 'react-router-dom'; +import { getContainerByName } from 'rest/objectStoreAPI'; +import ContainerPage from './ContainerPage'; +import { CONTAINER_DATA } from './ContainerPage.mock'; + +jest.mock('components/PermissionProvider/PermissionProvider', () => ({ + usePermissionProvider: jest.fn().mockImplementation(() => ({ + getEntityPermissionByFqn: jest.fn().mockResolvedValue({ + Create: true, + Delete: true, + EditAll: true, + EditCustomFields: true, + EditDataProfile: true, + EditDescription: true, + EditDisplayName: true, + EditLineage: true, + EditOwner: true, + EditQueries: true, + EditSampleData: true, + EditTags: true, + EditTests: true, + EditTier: true, + ViewAll: true, + ViewDataProfile: true, + ViewQueries: true, + ViewSampleData: true, + ViewTests: true, + ViewUsage: true, + }), + })), +})); + +jest.mock('components/common/CustomPropertyTable/CustomPropertyTable', () => { + return { + CustomPropertyTable: jest + .fn() + .mockReturnValue( +
CustomPropertyTable
+ ), + }; +}); + +jest.mock('components/common/description/Description', () => { + return jest + .fn() + .mockReturnValue(
Description
); +}); + +jest.mock('components/common/entityPageInfo/EntityPageInfo', () => { + return jest + .fn() + .mockReturnValue(
EntityPageInfo
); +}); + +jest.mock('components/common/error-with-placeholder/ErrorPlaceHolder', () => { + return jest + .fn() + .mockReturnValue( +
ErrorPlaceHolder
+ ); +}); + +jest.mock( + 'components/ContainerDetail/ContainerChildren/ContainerChildren', + () => { + return jest + .fn() + .mockReturnValue( +
ContainerChildren
+ ); + } +); + +jest.mock( + 'components/ContainerDetail/ContainerDataModel/ContainerDataModel', + () => { + return jest + .fn() + .mockReturnValue( +
ContainerDataModel
+ ); + } +); + +jest.mock('components/containers/PageContainerV1', () => { + return jest + .fn() + .mockImplementation(({ children }) => ( +
{children}
+ )); +}); + +jest.mock('components/EntityLineage/EntityLineage.component', () => { + return jest + .fn() + .mockReturnValue(
EntityLineage
); +}); + +jest.mock('components/Loader/Loader', () => { + return jest.fn().mockReturnValue(
Loader
); +}); + +jest.mock('rest/lineageAPI', () => ({ + getLineageByFQN: jest.fn().mockImplementation(() => Promise.resolve()), +})); + +jest.mock('rest/miscAPI', () => ({ + deleteLineageEdge: jest.fn().mockImplementation(() => Promise.resolve()), + addLineage: jest.fn().mockImplementation(() => Promise.resolve()), +})); + +jest.mock('rest/objectStoreAPI', () => ({ + addContainerFollower: jest.fn().mockImplementation(() => Promise.resolve()), + getContainerByName: jest + .fn() + .mockImplementation(() => Promise.resolve(CONTAINER_DATA)), + patchContainerDetails: jest.fn().mockImplementation(() => Promise.resolve()), + removeContainerFollower: jest + .fn() + .mockImplementation(() => Promise.resolve()), + restoreContainer: jest.fn().mockImplementation(() => Promise.resolve()), +})); + +let mockParams = { + entityFQN: 'entityTypeFQN', + tab: 'schema', +}; + +jest.mock('react-router-dom', () => ({ + useHistory: jest.fn(), + useParams: jest.fn().mockImplementation(() => mockParams), +})); + +describe('Container Page Component', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should render the child components', async () => { + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + + const pageTopInfo = screen.getByTestId('entity-page-info'); + const tabs = screen.getAllByRole('tab'); + + expect(pageTopInfo).toBeInTheDocument(); + expect(tabs).toHaveLength(4); + }); + + it('Should render the schema tab component', async () => { + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + const tabs = screen.getAllByRole('tab'); + + const schemaTab = tabs[0]; + + expect(schemaTab).toHaveAttribute('aria-selected', 'true'); + + const description = screen.getByTestId('description'); + + expect(description).toBeInTheDocument(); + + const dataModel = screen.getByTestId('container-data-model'); + + expect(dataModel).toBeInTheDocument(); + }); + + it('Should render the children tab component', async () => { + mockParams = { ...mockParams, tab: 'children' }; + + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + + const containerChildren = screen.getByTestId('containers-children'); + + expect(containerChildren).toBeInTheDocument(); + }); + + it('Should render the lineage tab component', async () => { + mockParams = { ...mockParams, tab: 'lineage' }; + + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + + const lineage = screen.getByTestId('entity-lineage'); + + expect(lineage).toBeInTheDocument(); + }); + + it('Should render the custom properties tab component', async () => { + mockParams = { ...mockParams, tab: 'custom-properties' }; + + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + + const customPropertyTable = screen.getByTestId('custom-properties-table'); + + expect(customPropertyTable).toBeInTheDocument(); + }); + + it('Should render error placeholder on API fail', async () => { + mockParams = { ...mockParams, tab: 'schema' }; + (getContainerByName as jest.Mock).mockImplementationOnce(() => + Promise.reject() + ); + + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + + const errorPlaceholder = screen.getByTestId('error-placeholder'); + + expect(errorPlaceholder).toBeInTheDocument(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx index d841f7b4737..b367da798da 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ContainerPage/ContainerPage.tsx @@ -37,7 +37,6 @@ import { } from 'components/PermissionProvider/PermissionProvider.interface'; import { FQN_SEPARATOR_CHAR } from 'constants/char.constants'; import { getServiceDetailsPath } from 'constants/constants'; -import { ENTITY_CARD_CLASS } from 'constants/entity.constants'; import { NO_PERMISSION_TO_VIEW } from 'constants/HelperTextUtil'; import { EntityInfo, EntityType } from 'enums/entity.enum'; import { ServiceCategory } from 'enums/service.enum'; @@ -426,7 +425,7 @@ const ContainerPage = () => { const { tags: newTags, version } = await handleUpdateContainerData({ ...(containerData as Container), tags: [ - ...(containerData?.tags ?? []), + ...getTagsWithoutTier(containerData?.tags ?? []), { tagFQN: updatedTier, labelType: LabelType.Manual, @@ -705,7 +704,7 @@ const ContainerPage = () => { }> { {t('label.custom-property-plural')} }> - + entity?.id || ''; export const getEntityTags = ( type: string, - entityDetail: Table | Pipeline | Dashboard | Topic | Mlmodel + entityDetail: EntityDetailUnion ): Array => { switch (type) { case EntityType.TABLE: { @@ -434,25 +433,30 @@ export const getEntityOverview = ( const { numberOfObjects, serviceType, dataModel } = entityDetail as Container; + const visible = [ + DRAWER_NAVIGATION_OPTIONS.lineage, + DRAWER_NAVIGATION_OPTIONS.explore, + ]; + const overview = [ { name: i18next.t('label.number-of-object'), value: numberOfObjects, isLink: false, - visible: [DRAWER_NAVIGATION_OPTIONS.explore], + visible, }, { name: i18next.t('label.service-type'), value: serviceType, isLink: false, - visible: [DRAWER_NAVIGATION_OPTIONS.explore], + visible, }, { name: i18next.t('label.column-plural'), value: dataModel && dataModel.columns ? dataModel.columns.length : NO_DATA, isLink: false, - visible: [DRAWER_NAVIGATION_OPTIONS.explore], + visible, }, ]; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx index 9e4d7045006..838419bb00e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -14,6 +14,7 @@ import Icon from '@ant-design/icons'; import { Tooltip } from 'antd'; import { ExpandableConfig } from 'antd/lib/table/interface'; +import { ReactComponent as ContainerIcon } from 'assets/svg/ic-object-store.svg'; import classNames from 'classnames'; import { t } from 'i18next'; import { upperCase } from 'lodash'; @@ -321,6 +322,10 @@ export const getEntityIcon = (indexType: string) => { case EntityType.PIPELINE: return ; + case SearchIndex.CONTAINER: + case EntityType.CONTAINER: + return ; + case SearchIndex.TABLE: case EntityType.TABLE: default: