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
This commit is contained in:
Sachin Chaurasiya 2023-03-24 18:13:44 +05:30 committed by GitHub
parent fb02cbfeed
commit c6746507d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 449 additions and 137 deletions

View File

@ -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<EntityData>(
{} as EntityData
const [entityDetail, setEntityDetail] = useState<EntityDetailUnion>(
{} as EntityDetailUnion
);
const [isLoading, setIsLoading] = useState<boolean>(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 (
<ContainerSummary
componentType={DRAWER_NAVIGATION_OPTIONS.lineage}
entityDetails={entityDetail as Container}
isLoading={isLoading}
tags={tags}
/>
);
default:
return null;

View File

@ -160,7 +160,10 @@ const SuccessScreen = ({
theme="primary"
variant="outlined"
onClick={handleViewServiceClick}>
<span>{viewServiceText ?? 'View Service'}</span>
<span>
{viewServiceText ??
t('label.view-entity', { entity: t('label.service') })}
</span>
</Button>
{showIngestionButton && (

View File

@ -247,4 +247,12 @@ declare module 'Models' {
}
export type PagingWithoutTotal = Omit<Paging, 'total'>;
type EntityDetailUnion =
| Table
| Pipeline
| Dashboard
| Topic
| Mlmodel
| Container;
}

View File

@ -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,
};

View File

@ -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(
<div data-testid="custom-properties-table">CustomPropertyTable</div>
),
};
});
jest.mock('components/common/description/Description', () => {
return jest
.fn()
.mockReturnValue(<div data-testid="description">Description</div>);
});
jest.mock('components/common/entityPageInfo/EntityPageInfo', () => {
return jest
.fn()
.mockReturnValue(<div data-testid="entity-page-info">EntityPageInfo</div>);
});
jest.mock('components/common/error-with-placeholder/ErrorPlaceHolder', () => {
return jest
.fn()
.mockReturnValue(
<div data-testid="error-placeholder">ErrorPlaceHolder</div>
);
});
jest.mock(
'components/ContainerDetail/ContainerChildren/ContainerChildren',
() => {
return jest
.fn()
.mockReturnValue(
<div data-testid="containers-children">ContainerChildren</div>
);
}
);
jest.mock(
'components/ContainerDetail/ContainerDataModel/ContainerDataModel',
() => {
return jest
.fn()
.mockReturnValue(
<div data-testid="container-data-model">ContainerDataModel</div>
);
}
);
jest.mock('components/containers/PageContainerV1', () => {
return jest
.fn()
.mockImplementation(({ children }) => (
<div data-testid="container-children">{children}</div>
));
});
jest.mock('components/EntityLineage/EntityLineage.component', () => {
return jest
.fn()
.mockReturnValue(<div data-testid="entity-lineage">EntityLineage</div>);
});
jest.mock('components/Loader/Loader', () => {
return jest.fn().mockReturnValue(<div data-testid="loader">Loader</div>);
});
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(<ContainerPage />, {
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(<ContainerPage />, {
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(<ContainerPage />, {
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(<ContainerPage />, {
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(<ContainerPage />, {
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(<ContainerPage />, {
wrapper: MemoryRouter,
});
});
const errorPlaceholder = screen.getByTestId('error-placeholder');
expect(errorPlaceholder).toBeInTheDocument();
});
});

View File

@ -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 = () => {
</span>
}>
<Card
className={`${ENTITY_CARD_CLASS} card-body-full`}
className="h-full card-body-full"
data-testid="lineage-details">
<EntityLineageComponent
addLineageHandler={handleAddLineage}
@ -732,7 +731,7 @@ const ContainerPage = () => {
{t('label.custom-property-plural')}
</span>
}>
<Card className={ENTITY_CARD_CLASS}>
<Card className="h-full">
<CustomPropertyTable
entityDetails={
containerData as CustomPropertyProps['entityDetails']

View File

@ -24,7 +24,7 @@ import { Container } from 'generated/entity/data/container';
import { Mlmodel } from 'generated/entity/data/mlmodel';
import i18next from 'i18next';
import { isEmpty, isNil, isUndefined, lowerCase, startCase } from 'lodash';
import { Bucket } from 'Models';
import { Bucket, EntityDetailUnion } from 'Models';
import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
@ -48,7 +48,6 @@ import {
Table,
TableType,
} from '../generated/entity/data/table';
import { Topic } from '../generated/entity/data/topic';
import { Edge, EntityLineage } from '../generated/type/entityLineage';
import { EntityReference } from '../generated/type/entityUsage';
import { TagLabel } from '../generated/type/tagLabel';
@ -85,7 +84,7 @@ export const getEntityId = (entity?: { id?: string }) => entity?.id || '';
export const getEntityTags = (
type: string,
entityDetail: Table | Pipeline | Dashboard | Topic | Mlmodel
entityDetail: EntityDetailUnion
): Array<TagLabel> => {
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,
},
];

View File

@ -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 <PipelineIcon />;
case SearchIndex.CONTAINER:
case EntityType.CONTAINER:
return <ContainerIcon />;
case SearchIndex.TABLE:
case EntityType.TABLE:
default: