mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-26 17:34:41 +00:00
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:
parent
b89cf64f14
commit
9bb4b18628
@ -19,13 +19,12 @@ import {
|
||||
import { TAGS_ADD_REMOVE_ENTITIES } from '../../constants/tagsAddRemove.constants';
|
||||
|
||||
const addTags = (tag) => {
|
||||
cy.get('[data-testid="tag-selector"]')
|
||||
.scrollIntoView()
|
||||
.should('be.visible')
|
||||
.click()
|
||||
.type(tag.split('.')[1]);
|
||||
const tagName = Cypress._.split(tag, '.')[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);
|
||||
};
|
||||
|
||||
|
@ -53,8 +53,10 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { getActiveAnnouncement, getFeedCount } from 'rest/feedsAPI';
|
||||
import { getContainerByName } from 'rest/storageAPI';
|
||||
import { getCurrentUserId, getEntityDetailLink } from 'utils/CommonUtils';
|
||||
import {
|
||||
getBreadcrumbForContainer,
|
||||
getBreadcrumbForEntitiesWithServiceOnly,
|
||||
getBreadcrumbForTable,
|
||||
getEntityBreadcrumbs,
|
||||
@ -128,6 +130,8 @@ export const DataAssetsHeader = ({
|
||||
const { isTourPage } = useTourProvider();
|
||||
const { onCopyToClipBoard } = useClipboard(window.location.href);
|
||||
const [taskCount, setTaskCount] = useState(0);
|
||||
const [parentContainers, setParentContainers] = useState<Container[]>([]);
|
||||
const [isBreadcrumbLoading, setIsBreadcrumbLoading] = useState(false);
|
||||
const history = useHistory();
|
||||
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(() => {
|
||||
if (dataAsset.fullyQualifiedName && !isTourPage) {
|
||||
fetchActiveAnnouncement();
|
||||
fetchTaskCount();
|
||||
}
|
||||
}, [dataAsset.fullyQualifiedName]);
|
||||
if (entityType === EntityType.CONTAINER) {
|
||||
const asset = dataAsset as Container;
|
||||
fetchContainerParent(asset.parent?.fullyQualifiedName ?? '');
|
||||
}
|
||||
}, [dataAsset]);
|
||||
|
||||
const { extraInfo, breadcrumbs }: DataAssetHeaderInfo = useMemo(() => {
|
||||
const returnData: DataAssetHeaderInfo = {
|
||||
@ -354,8 +388,10 @@ export const DataAssetsHeader = ({
|
||||
</>
|
||||
);
|
||||
|
||||
returnData.breadcrumbs =
|
||||
getBreadcrumbForEntitiesWithServiceOnly(containerDetails);
|
||||
returnData.breadcrumbs = getBreadcrumbForContainer({
|
||||
entity: containerDetails,
|
||||
parents: parentContainers,
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
@ -430,7 +466,7 @@ export const DataAssetsHeader = ({
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}, [dataAsset, entityType]);
|
||||
}, [dataAsset, entityType, parentContainers]);
|
||||
|
||||
const handleOpenTaskClick = () => {
|
||||
if (!dataAsset.fullyQualifiedName) {
|
||||
@ -460,7 +496,10 @@ export const DataAssetsHeader = ({
|
||||
<Col className="self-center" span={18}>
|
||||
<Row gutter={[16, 12]}>
|
||||
<Col span={24}>
|
||||
<TitleBreadcrumb titleLinks={breadcrumbs} />
|
||||
<TitleBreadcrumb
|
||||
loading={isBreadcrumbLoading}
|
||||
titleLinks={breadcrumbs}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<EntityHeaderTitle
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -10,29 +10,32 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Skeleton } from 'antd';
|
||||
import { Col, Row, Skeleton } from 'antd';
|
||||
import { uniqueId } from 'lodash';
|
||||
import React from 'react';
|
||||
import { TitleBreadcrumbSkeletonProps } from '../Skeleton.interfaces';
|
||||
|
||||
const TitleBreadcrumbSkeleton = ({
|
||||
titleLinks,
|
||||
loading,
|
||||
children,
|
||||
}: TitleBreadcrumbSkeletonProps) =>
|
||||
titleLinks.length === 0 ? (
|
||||
<div className="flex">
|
||||
{titleLinks.map(() => (
|
||||
loading ? (
|
||||
<Row gutter={16}>
|
||||
{Array(3)
|
||||
.fill(null)
|
||||
.map(() => (
|
||||
<Col key={uniqueId()}>
|
||||
<Skeleton
|
||||
active
|
||||
className="m-l-xs"
|
||||
key={uniqueId()}
|
||||
paragraph={{ rows: 0 }}
|
||||
title={{
|
||||
width: 150,
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
))}
|
||||
</div>
|
||||
</Row>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
|
@ -12,7 +12,6 @@
|
||||
*/
|
||||
import { SkeletonProps } from 'antd';
|
||||
import { SkeletonButtonProps } from 'antd/lib/skeleton/Button';
|
||||
import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface';
|
||||
|
||||
export interface Key {
|
||||
key?: React.Key | null | undefined;
|
||||
@ -26,7 +25,7 @@ export interface SkeletonInterface extends Children {
|
||||
dataLength?: number;
|
||||
}
|
||||
export interface TitleBreadcrumbSkeletonProps extends Children {
|
||||
titleLinks: TitleBreadcrumbProps['titleLinks'];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export interface ButtonSkeletonProps
|
||||
|
@ -28,6 +28,7 @@ const TitleBreadcrumb: FunctionComponent<TitleBreadcrumbProps> = ({
|
||||
titleLinks,
|
||||
className = '',
|
||||
noLink = false,
|
||||
loading = false,
|
||||
widthDeductions,
|
||||
}: TitleBreadcrumbProps) => {
|
||||
const { t } = useTranslation();
|
||||
@ -59,7 +60,7 @@ const TitleBreadcrumb: FunctionComponent<TitleBreadcrumbProps> = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TitleBreadcrumbSkeleton titleLinks={titleLinks}>
|
||||
<TitleBreadcrumbSkeleton loading={loading}>
|
||||
<nav className={className} data-testid="breadcrumb">
|
||||
<ol className="rounded-4 d-flex flex-wrap">
|
||||
{titleLinks.map((link, index) => {
|
||||
|
@ -20,5 +20,6 @@ export type TitleBreadcrumbProps = {
|
||||
}>;
|
||||
className?: string;
|
||||
noLink?: boolean;
|
||||
loading?: boolean;
|
||||
widthDeductions?: number;
|
||||
};
|
||||
|
@ -105,7 +105,6 @@ const ContainerPage = () => {
|
||||
const [isEditDescription, setIsEditDescription] = useState<boolean>(false);
|
||||
const [isLineageLoading, setIsLineageLoading] = useState<boolean>(false);
|
||||
|
||||
const [, setParentContainers] = useState<Container[]>([]);
|
||||
const [containerData, setContainerData] = useState<Container>();
|
||||
const [containerChildrenData, setContainerChildrenData] = useState<
|
||||
Container['children']
|
||||
@ -134,24 +133,6 @@ const ContainerPage = () => {
|
||||
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) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@ -172,9 +153,6 @@ const ContainerPage = () => {
|
||||
...response,
|
||||
tags: sortTagsCaseInsensitive(response.tags || []),
|
||||
});
|
||||
if (response.parent && response.parent.fullyQualifiedName) {
|
||||
await fetchContainerParent(response.parent.fullyQualifiedName, true);
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
setHasError(true);
|
||||
@ -795,8 +773,6 @@ const ContainerPage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
fetchResourcePermission(containerName);
|
||||
// reset parent containers list on containername change
|
||||
setParentContainers([]);
|
||||
}, [containerName]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -915,6 +915,35 @@ export const getEntityReferenceListFromEntities = <
|
||||
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 = (
|
||||
entity: Table,
|
||||
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 = (
|
||||
entity:
|
||||
| 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 = (
|
||||
entityFieldThreadCount: EntityFieldThreadCount[]
|
||||
) => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user