fix(ui): parent container is not populating in breadcrumb (#12294)

* fix(ui): parent container is not populating in breadcrumb

* fixed cypress for tagsAddRemove spec

* updated parent hierarchy in breadcrumb for container

* move fetch logic from container page to data header component

* addressing comments

* removed unwanted prop and added unit test for the change
This commit is contained in:
Shailesh Parmar 2023-07-12 22:19:26 +05:30 committed by GitHub
parent b89cf64f14
commit 9bb4b18628
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 261 additions and 83 deletions

View File

@ -19,13 +19,12 @@ import {
import { TAGS_ADD_REMOVE_ENTITIES } from '../../constants/tagsAddRemove.constants'; import { TAGS_ADD_REMOVE_ENTITIES } from '../../constants/tagsAddRemove.constants';
const addTags = (tag) => { const addTags = (tag) => {
cy.get('[data-testid="tag-selector"]') const tagName = Cypress._.split(tag, '.')[1];
.scrollIntoView()
.should('be.visible')
.click()
.type(tag.split('.')[1]);
cy.get(`[data-testid='tag-${tag}']`).should('be.visible').click(); cy.get('[data-testid="tag-selector"]').scrollIntoView().should('be.visible');
cy.get('[data-testid="tag-selector"]').click().type(tagName);
cy.get(`[data-testid='tag-${tag}']`).click();
cy.get('[data-testid="tag-selector"] > .ant-select-selector').contains(tag); cy.get('[data-testid="tag-selector"] > .ant-select-selector').contains(tag);
}; };

View File

@ -53,8 +53,10 @@ import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { getActiveAnnouncement, getFeedCount } from 'rest/feedsAPI'; import { getActiveAnnouncement, getFeedCount } from 'rest/feedsAPI';
import { getContainerByName } from 'rest/storageAPI';
import { getCurrentUserId, getEntityDetailLink } from 'utils/CommonUtils'; import { getCurrentUserId, getEntityDetailLink } from 'utils/CommonUtils';
import { import {
getBreadcrumbForContainer,
getBreadcrumbForEntitiesWithServiceOnly, getBreadcrumbForEntitiesWithServiceOnly,
getBreadcrumbForTable, getBreadcrumbForTable,
getEntityBreadcrumbs, getEntityBreadcrumbs,
@ -128,6 +130,8 @@ export const DataAssetsHeader = ({
const { isTourPage } = useTourProvider(); const { isTourPage } = useTourProvider();
const { onCopyToClipBoard } = useClipboard(window.location.href); const { onCopyToClipBoard } = useClipboard(window.location.href);
const [taskCount, setTaskCount] = useState(0); const [taskCount, setTaskCount] = useState(0);
const [parentContainers, setParentContainers] = useState<Container[]>([]);
const [isBreadcrumbLoading, setIsBreadcrumbLoading] = useState(false);
const history = useHistory(); const history = useHistory();
const icon = useMemo( const icon = useMemo(
() => () =>
@ -196,12 +200,42 @@ export const DataAssetsHeader = ({
}); });
}; };
const fetchContainerParent = async (
parentName: string,
parents = [] as Container[]
) => {
if (isEmpty(parentName)) {
return;
}
setIsBreadcrumbLoading(true);
try {
const response = await getContainerByName(parentName, 'parent');
const updatedParent = [response, ...parents];
if (response?.parent?.fullyQualifiedName) {
await fetchContainerParent(
response.parent.fullyQualifiedName,
updatedParent
);
} else {
setParentContainers(updatedParent);
}
} catch (error) {
showErrorToast(error as AxiosError, t('server.unexpected-response'));
} finally {
setIsBreadcrumbLoading(false);
}
};
useEffect(() => { useEffect(() => {
if (dataAsset.fullyQualifiedName && !isTourPage) { if (dataAsset.fullyQualifiedName && !isTourPage) {
fetchActiveAnnouncement(); fetchActiveAnnouncement();
fetchTaskCount(); fetchTaskCount();
} }
}, [dataAsset.fullyQualifiedName]); if (entityType === EntityType.CONTAINER) {
const asset = dataAsset as Container;
fetchContainerParent(asset.parent?.fullyQualifiedName ?? '');
}
}, [dataAsset]);
const { extraInfo, breadcrumbs }: DataAssetHeaderInfo = useMemo(() => { const { extraInfo, breadcrumbs }: DataAssetHeaderInfo = useMemo(() => {
const returnData: DataAssetHeaderInfo = { const returnData: DataAssetHeaderInfo = {
@ -354,8 +388,10 @@ export const DataAssetsHeader = ({
</> </>
); );
returnData.breadcrumbs = returnData.breadcrumbs = getBreadcrumbForContainer({
getBreadcrumbForEntitiesWithServiceOnly(containerDetails); entity: containerDetails,
parents: parentContainers,
});
break; break;
@ -430,7 +466,7 @@ export const DataAssetsHeader = ({
} }
return returnData; return returnData;
}, [dataAsset, entityType]); }, [dataAsset, entityType, parentContainers]);
const handleOpenTaskClick = () => { const handleOpenTaskClick = () => {
if (!dataAsset.fullyQualifiedName) { if (!dataAsset.fullyQualifiedName) {
@ -460,7 +496,10 @@ export const DataAssetsHeader = ({
<Col className="self-center" span={18}> <Col className="self-center" span={18}>
<Row gutter={[16, 12]}> <Row gutter={[16, 12]}>
<Col span={24}> <Col span={24}>
<TitleBreadcrumb titleLinks={breadcrumbs} /> <TitleBreadcrumb
loading={isBreadcrumbLoading}
titleLinks={breadcrumbs}
/>
</Col> </Col>
<Col span={24}> <Col span={24}>
<EntityHeaderTitle <EntityHeaderTitle

View File

@ -0,0 +1,117 @@
/*
* 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 { render } from '@testing-library/react';
import { EntityType } from 'enums/entity.enum';
import { Container } from 'generated/entity/data/container';
import React from 'react';
import { getContainerByName } from 'rest/storageAPI';
import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils';
import { DataAssetsHeader } from './DataAssetsHeader.component';
import { DataAssetsHeaderProps } from './DataAssetsHeader.interface';
const mockProps: DataAssetsHeaderProps = {
dataAsset: {
id: 'assets-id',
name: 'testContainer',
parent: {
id: 'id',
type: 'container',
fullyQualifiedName: 'fullyQualifiedName',
},
service: {
id: 'service-id',
name: 's3_storage_sample',
type: 'storageService',
},
} as Container,
entityType: EntityType.CONTAINER,
permissions: DEFAULT_ENTITY_PERMISSION,
onRestoreDataAsset: jest.fn(),
onDisplayNameUpdate: jest.fn(),
onFollowClick: jest.fn(),
onVersionClick: jest.fn(),
onTierUpdate: jest.fn(),
onOwnerUpdate: jest.fn(),
};
jest.mock(
'components/common/title-breadcrumb/title-breadcrumb.component',
() => {
return jest
.fn()
.mockImplementation(() => <div>TitleBreadcrumb.component</div>);
}
);
jest.mock(
'components/Entity/EntityHeaderTitle/EntityHeaderTitle.component',
() => {
return jest
.fn()
.mockImplementation(() => <div>EntityHeaderTitle.component</div>);
}
);
jest.mock('components/common/OwnerLabel/OwnerLabel.component', () => ({
OwnerLabel: jest
.fn()
.mockImplementation(() => <div>OwnerLabel.component</div>),
}));
jest.mock('components/common/TierCard/TierCard', () =>
jest.fn().mockImplementation(({ children }) => (
<div>
TierCard.component
<div>{children}</div>
</div>
))
);
jest.mock('components/common/entityPageInfo/ManageButton/ManageButton', () =>
jest.fn().mockImplementation(() => <div>ManageButton.component</div>)
);
jest.mock(
'components/common/entityPageInfo/AnnouncementCard/AnnouncementCard',
() =>
jest.fn().mockImplementation(() => <div>AnnouncementCard.component</div>)
);
jest.mock(
'components/common/entityPageInfo/AnnouncementDrawer/AnnouncementDrawer',
() =>
jest.fn().mockImplementation(() => <div>AnnouncementDrawer.component</div>)
);
jest.mock('rest/storageAPI', () => ({
getContainerByName: jest
.fn()
.mockImplementation(() => Promise.resolve({ name: 'test' })),
}));
describe('DataAssetsHeader component', () => {
it('should call getContainerByName API on Page load for container assets', () => {
const mockGetContainerByName = getContainerByName as jest.Mock;
render(<DataAssetsHeader {...mockProps} />);
expect(mockGetContainerByName).toHaveBeenCalledWith(
'fullyQualifiedName',
'parent'
);
});
it('should not call getContainerByName API if parent is undefined', () => {
const mockGetContainerByName = getContainerByName as jest.Mock;
render(
<DataAssetsHeader
{...mockProps}
dataAsset={{ ...mockProps.dataAsset, parent: undefined }}
/>
);
expect(mockGetContainerByName).not.toHaveBeenCalled();
});
});

View File

@ -10,29 +10,32 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { Skeleton } from 'antd'; import { Col, Row, Skeleton } from 'antd';
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import React from 'react'; import React from 'react';
import { TitleBreadcrumbSkeletonProps } from '../Skeleton.interfaces'; import { TitleBreadcrumbSkeletonProps } from '../Skeleton.interfaces';
const TitleBreadcrumbSkeleton = ({ const TitleBreadcrumbSkeleton = ({
titleLinks, loading,
children, children,
}: TitleBreadcrumbSkeletonProps) => }: TitleBreadcrumbSkeletonProps) =>
titleLinks.length === 0 ? ( loading ? (
<div className="flex"> <Row gutter={16}>
{titleLinks.map(() => ( {Array(3)
<Skeleton .fill(null)
active .map(() => (
className="m-l-xs" <Col key={uniqueId()}>
key={uniqueId()} <Skeleton
paragraph={{ rows: 0 }} active
title={{ className="m-l-xs"
width: 150, paragraph={{ rows: 0 }}
}} title={{
/> width: 150,
))} }}
</div> />
</Col>
))}
</Row>
) : ( ) : (
children children
); );

View File

@ -12,7 +12,6 @@
*/ */
import { SkeletonProps } from 'antd'; import { SkeletonProps } from 'antd';
import { SkeletonButtonProps } from 'antd/lib/skeleton/Button'; import { SkeletonButtonProps } from 'antd/lib/skeleton/Button';
import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface';
export interface Key { export interface Key {
key?: React.Key | null | undefined; key?: React.Key | null | undefined;
@ -26,7 +25,7 @@ export interface SkeletonInterface extends Children {
dataLength?: number; dataLength?: number;
} }
export interface TitleBreadcrumbSkeletonProps extends Children { export interface TitleBreadcrumbSkeletonProps extends Children {
titleLinks: TitleBreadcrumbProps['titleLinks']; loading: boolean;
} }
export interface ButtonSkeletonProps export interface ButtonSkeletonProps

View File

@ -28,6 +28,7 @@ const TitleBreadcrumb: FunctionComponent<TitleBreadcrumbProps> = ({
titleLinks, titleLinks,
className = '', className = '',
noLink = false, noLink = false,
loading = false,
widthDeductions, widthDeductions,
}: TitleBreadcrumbProps) => { }: TitleBreadcrumbProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -59,7 +60,7 @@ const TitleBreadcrumb: FunctionComponent<TitleBreadcrumbProps> = ({
}, []); }, []);
return ( return (
<TitleBreadcrumbSkeleton titleLinks={titleLinks}> <TitleBreadcrumbSkeleton loading={loading}>
<nav className={className} data-testid="breadcrumb"> <nav className={className} data-testid="breadcrumb">
<ol className="rounded-4 d-flex flex-wrap"> <ol className="rounded-4 d-flex flex-wrap">
{titleLinks.map((link, index) => { {titleLinks.map((link, index) => {

View File

@ -20,5 +20,6 @@ export type TitleBreadcrumbProps = {
}>; }>;
className?: string; className?: string;
noLink?: boolean; noLink?: boolean;
loading?: boolean;
widthDeductions?: number; widthDeductions?: number;
}; };

View File

@ -105,7 +105,6 @@ const ContainerPage = () => {
const [isEditDescription, setIsEditDescription] = useState<boolean>(false); const [isEditDescription, setIsEditDescription] = useState<boolean>(false);
const [isLineageLoading, setIsLineageLoading] = useState<boolean>(false); const [isLineageLoading, setIsLineageLoading] = useState<boolean>(false);
const [, setParentContainers] = useState<Container[]>([]);
const [containerData, setContainerData] = useState<Container>(); const [containerData, setContainerData] = useState<Container>();
const [containerChildrenData, setContainerChildrenData] = useState< const [containerChildrenData, setContainerChildrenData] = useState<
Container['children'] Container['children']
@ -134,24 +133,6 @@ const ContainerPage = () => {
ThreadType.Conversation ThreadType.Conversation
); );
// data fetching methods
const fetchContainerParent = async (
parentName: string,
newContainer = false
) => {
try {
const response = await getContainerByName(parentName, 'parent');
setParentContainers((prev) =>
newContainer ? [response] : [response, ...prev]
);
if (response.parent && response.parent.fullyQualifiedName) {
await fetchContainerParent(response.parent.fullyQualifiedName);
}
} catch (error) {
showErrorToast(error as AxiosError, t('server.unexpected-response'));
}
};
const fetchContainerDetail = async (containerFQN: string) => { const fetchContainerDetail = async (containerFQN: string) => {
setIsLoading(true); setIsLoading(true);
try { try {
@ -172,9 +153,6 @@ const ContainerPage = () => {
...response, ...response,
tags: sortTagsCaseInsensitive(response.tags || []), tags: sortTagsCaseInsensitive(response.tags || []),
}); });
if (response.parent && response.parent.fullyQualifiedName) {
await fetchContainerParent(response.parent.fullyQualifiedName, true);
}
} catch (error) { } catch (error) {
showErrorToast(error as AxiosError); showErrorToast(error as AxiosError);
setHasError(true); setHasError(true);
@ -795,8 +773,6 @@ const ContainerPage = () => {
useEffect(() => { useEffect(() => {
fetchResourcePermission(containerName); fetchResourcePermission(containerName);
// reset parent containers list on containername change
setParentContainers([]);
}, [containerName]); }, [containerName]);
useEffect(() => { useEffect(() => {

View File

@ -915,6 +915,35 @@ export const getEntityReferenceListFromEntities = <
return entities.map((entity) => getEntityReferenceFromEntity(entity, type)); return entities.map((entity) => getEntityReferenceFromEntity(entity, type));
}; };
export const getEntityLinkFromType = (
fullyQualifiedName: string,
entityType: EntityType
) => {
switch (entityType) {
case EntityType.TABLE:
return getTableDetailsPath(fullyQualifiedName);
case EntityType.GLOSSARY:
case EntityType.GLOSSARY_TERM:
return getGlossaryTermDetailsPath(fullyQualifiedName);
case EntityType.TAG:
return getTagsDetailsPath(fullyQualifiedName);
case EntityType.TOPIC:
return getTopicDetailsPath(fullyQualifiedName);
case EntityType.DASHBOARD:
return getDashboardDetailsPath(fullyQualifiedName);
case EntityType.PIPELINE:
return getPipelineDetailsPath(fullyQualifiedName);
case EntityType.MLMODEL:
return getMlModelDetailsPath(fullyQualifiedName);
case EntityType.CONTAINER:
return getContainerDetailPath(fullyQualifiedName);
case EntityType.DATABASE:
return getDatabaseDetailsPath(fullyQualifiedName);
default:
return '';
}
};
export const getBreadcrumbForTable = ( export const getBreadcrumbForTable = (
entity: Table, entity: Table,
includeCurrent = false includeCurrent = false
@ -987,6 +1016,49 @@ export const getBreadcrumbForEntitiesWithServiceOnly = (
]; ];
}; };
export const getBreadcrumbForContainer = (data: {
entity: Container;
includeCurrent?: boolean;
parents?: Container[];
}) => {
const { entity, includeCurrent = false, parents = [] } = data;
const { service } = entity;
return [
{
name: getEntityName(service),
url: service?.name
? getServiceDetailsPath(
service?.name,
ServiceCategoryPlural[
service?.type as keyof typeof ServiceCategoryPlural
]
)
: '',
},
...(parents.length > 0
? parents.map((parent) => ({
name: getEntityName(parent),
url: getEntityLinkFromType(
parent?.fullyQualifiedName ?? '',
EntityType.CONTAINER
),
}))
: []),
...(includeCurrent
? [
{
name: entity.name,
url: getEntityLinkFromType(
entity.fullyQualifiedName ?? '',
(entity as SourceType).entityType as EntityType
),
},
]
: []),
];
};
export const getEntityBreadcrumbs = ( export const getEntityBreadcrumbs = (
entity: entity:
| SearchedDataProps['data'][number]['_source'] | SearchedDataProps['data'][number]['_source']
@ -1061,35 +1133,6 @@ export const getEntityBreadcrumbs = (
} }
}; };
export const getEntityLinkFromType = (
fullyQualifiedName: string,
entityType: EntityType
) => {
switch (entityType) {
case EntityType.TABLE:
return getTableDetailsPath(fullyQualifiedName);
case EntityType.GLOSSARY:
case EntityType.GLOSSARY_TERM:
return getGlossaryTermDetailsPath(fullyQualifiedName);
case EntityType.TAG:
return getTagsDetailsPath(fullyQualifiedName);
case EntityType.TOPIC:
return getTopicDetailsPath(fullyQualifiedName);
case EntityType.DASHBOARD:
return getDashboardDetailsPath(fullyQualifiedName);
case EntityType.PIPELINE:
return getPipelineDetailsPath(fullyQualifiedName);
case EntityType.MLMODEL:
return getMlModelDetailsPath(fullyQualifiedName);
case EntityType.CONTAINER:
return getContainerDetailPath(fullyQualifiedName);
case EntityType.DATABASE:
return getDatabaseDetailsPath(fullyQualifiedName);
default:
return '';
}
};
export const getEntityThreadLink = ( export const getEntityThreadLink = (
entityFieldThreadCount: EntityFieldThreadCount[] entityFieldThreadCount: EntityFieldThreadCount[]
) => { ) => {