feat(ui): breadcrumb style updates (#10974)

* feat(ui): breadcrumb style updates
entity title styling

* fix localisation

* update explore card & summary panel breadcrumbs

* fix breadcrumbs for container, glossary & tags

* update breadcrumb styling at all the places

* fix service type missing from version components

* fix unit tests

* fix breadcrumb styling
fix icons related changes
fix link related issue

* fixed assets related feedbacks

* add external link for assets

* fix cypress

* fix breadcrumbs on the glossary

* fix glossary breadcrumb issue

* update databaseSchema page layout

* fix code smells

* fix code smells

* fix unit tests failing

* update tag icon and fix lineage cypress

* fix restore entity

* fix restore entity spec

* fix test id issue
This commit is contained in:
Chirag Madlani 2023-04-14 11:28:11 +05:30 committed by GitHub
parent 91b04ee9a3
commit c89bcb51b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 1024 additions and 935 deletions

View File

@ -323,7 +323,7 @@ export const deleteCreatedService = (
.should('be.visible')
.click();
cy.get(`[data-testid="inactive-link"]`)
cy.get(`[data-testid="entity-header-name"]`)
.should('exist')
.should('be.visible')
.invoke('text')

View File

@ -79,11 +79,12 @@ describe('Restore entity functionality should work properly', () => {
cy.get('[data-testid="show-deleted"]').should('exist').click();
verifyResponseStatusCode('@showDeletedTables', 200);
cy.get('[data-testid="sample_data-raw_product_catalog"]')
cy.get('[data-testid="entity-header-display-name"]')
.contains('raw_product_catalog')
.should('exist')
.click();
cy.get('[data-testid="inactive-link"]')
cy.get('[data-testid="entity-header-display-name"]')
.should('be.visible')
.contains(ENTITY_TABLE.displayName);
@ -96,18 +97,20 @@ describe('Restore entity functionality should work properly', () => {
cy.get('[data-testid="show-deleted"]').should('exist').click();
verifyResponseStatusCode('@showDeletedTables', 200);
cy.get('[data-testid="sample_data-raw_product_catalog"]')
cy.get('[data-testid="entity-header-display-name"]')
.contains('raw_product_catalog')
.should('exist')
.click();
cy.get('[data-testid="inactive-link"]')
cy.get('[data-testid="entity-header-display-name"]')
.should('be.visible')
.contains(ENTITY_TABLE.displayName)
.click();
cy.get('[data-testid="deleted-badge"]').should('exist');
cy.get('[data-testid="breadcrumb-link"]')
cy.get('[data-testid="breadcrumb"]')
.scrollIntoView()
.should('be.visible')
.contains(ENTITY_TABLE.schemaName)
.click();
@ -142,11 +145,12 @@ describe('Restore entity functionality should work properly', () => {
cy.get('[data-testid="show-deleted"]').should('exist').click();
verifyResponseStatusCode('@showDeletedTables', 200);
cy.get('[data-testid="sample_data-raw_product_catalog"]')
cy.get('[data-testid="entity-header-display-name"]')
.contains('raw_product_catalog')
.should('exist')
.click();
cy.get('[data-testid="inactive-link"]')
cy.get('[data-testid="entity-header-display-name"]')
.should('be.visible')
.contains(ENTITY_TABLE.displayName);

View File

@ -582,7 +582,9 @@ describe('Data Quality and Profiler should work properly', () => {
const { term, entity, serviceName, testCaseName } =
DATA_QUALITY_SAMPLE_DATA_TABLE;
visitEntityDetailsPage(term, serviceName, entity);
cy.get('[data-testid="inactive-link"]').should('be.visible').contains(term);
cy.get('[data-testid="entity-header-name"]')
.should('be.visible')
.contains(term);
cy.get('[data-testid="Profiler & Data Quality"]')
.should('be.visible')
.click();
@ -633,7 +635,9 @@ describe('Data Quality and Profiler should work properly', () => {
);
visitEntityDetailsPage(term, serviceName, entity);
verifyResponseStatusCode('@waitForPageLoad', 200);
cy.get('[data-testid="inactive-link"]').should('be.visible').contains(term);
cy.get('[data-testid="entity-header-name"]')
.should('be.visible')
.contains(term);
cy.get('[data-testid="Profiler & Data Quality"]')
.should('be.visible')
.click();

View File

@ -704,7 +704,7 @@ describe('Glossary page should work properly', () => {
false
);
cy.get(`[data-testid="${entity.serviceName}-${entity.term}"]`)
cy.get('[data-testid="entity-header-display-name"]')
.contains(entity.term)
.should('be.visible');
});
@ -722,7 +722,8 @@ describe('Glossary page should work properly', () => {
verifyResponseStatusCode('@assetTab', 200);
interceptURL('GET', '/api/v1/feed*', 'entityDetails');
cy.get(`[data-testid="${entity.serviceName}-${entity.term}"]`)
cy.get('[data-testid="entity-header-display-name"]')
.contains(entity.term)
.should('be.visible')
.click();

View File

@ -88,7 +88,7 @@ describe('MyData page should work', () => {
entity.entityObj.serviceName,
entity.entityObj.entity
);
cy.get('[data-testid="inactive-link"]')
cy.get('[data-testid="entity-header-display-name"]')
.invoke('text')
.then((newText) => {
expect(newText).equal(text);
@ -98,7 +98,7 @@ describe('MyData page should work', () => {
.contains(text)
.should('be.visible')
.click();
cy.get('[data-testid="inactive-link"]')
cy.get('[data-testid="entity-header-display-name"]')
.invoke('text')
.then((newText) => {
expect(newText).equal(text);

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g fill="#37352F" stroke="#37352F" stroke-width=".1" clip-path="url(#a)"><path d="M13.461 8.077a.461.461 0 0 0-.461.461v4.154a1.385 1.385 0 0 1-1.385 1.385H3.308a1.385 1.385 0 0 1-1.385-1.385V4.385A1.385 1.385 0 0 1 3.308 3h4.154a.462.462 0 1 0 0-.923H3.308A2.308 2.308 0 0 0 1 4.385v8.307A2.308 2.308 0 0 0 3.308 15h8.307a2.308 2.308 0 0 0 2.308-2.308V8.538a.461.461 0 0 0-.462-.461Z"/><path d="M10.385 2.154h2.648l-6.52 6.513a.577.577 0 0 0 .188.946.577.577 0 0 0 .632-.126l6.513-6.514v2.642a.577.577 0 0 0 1.154 0V1.577a.578.578 0 0 0-.355-.534.577.577 0 0 0-.222-.043h-4.038a.577.577 0 1 0 0 1.154Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none"><g fill="#37352F" stroke="#37352F" stroke-width=".1" clip-path="url(#a)"><path d="M13.461 8.077a.461.461 0 0 0-.461.461v4.154a1.385 1.385 0 0 1-1.385 1.385H3.308a1.385 1.385 0 0 1-1.385-1.385V4.385A1.385 1.385 0 0 1 3.308 3h4.154a.462.462 0 1 0 0-.923H3.308A2.308 2.308 0 0 0 1 4.385v8.307A2.308 2.308 0 0 0 3.308 15h8.307a2.308 2.308 0 0 0 2.308-2.308V8.538a.461.461 0 0 0-.462-.461Z"/><path d="M10.385 2.154h2.648l-6.52 6.513a.577.577 0 0 0 .188.946.577.577 0 0 0 .632-.126l6.513-6.514v2.642a.577.577 0 0 0 1.154 0V1.577a.578.578 0 0 0-.355-.534.577.577 0 0 0-.222-.043h-4.038a.577.577 0 1 0 0 1.154Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 768 B

After

Width:  |  Height:  |  Size: 765 B

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none"><path fill="#7147E8" d="M6.856 12c.376 0 .73-.147.995-.413l3.739-3.744a1.409 1.409 0 0 0 0-1.987L6.55.807A2.731 2.731 0 0 0 4.604 0H1.406C.631 0 0 .63 0 1.406v3.188c0 .735.286 1.425.806 1.945l5.056 5.05c.266.265.619.411.994.411ZM4.604.937a1.8 1.8 0 0 1 1.282.532l5.04 5.05a.47.47 0 0 1 0 .662l-3.739 3.744a.466.466 0 0 1-.33.137h-.001a.466.466 0 0 1-.331-.136l-5.056-5.05a1.8 1.8 0 0 1-.531-1.282V1.406a.47.47 0 0 1 .468-.468h3.198Zm-1.205 3.82c.775 0 1.406-.63 1.406-1.405 0-.776-.63-1.407-1.406-1.407a1.408 1.408 0 0 0 0 2.813Zm0-1.874a.47.47 0 1 1-.001.938.47.47 0 0 1 0-.938Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none"><path fill="#7147E8" d="M6.856 12c.376 0 .73-.147.995-.413l3.739-3.744a1.409 1.409 0 0 0 0-1.987L6.55.807A2.731 2.731 0 0 0 4.604 0H1.406C.631 0 0 .63 0 1.406v3.188c0 .735.286 1.425.806 1.945l5.056 5.05c.266.265.619.411.994.411ZM4.604.937a1.8 1.8 0 0 1 1.282.532l5.04 5.05a.47.47 0 0 1 0 .662l-3.739 3.744a.466.466 0 0 1-.33.137h-.001a.466.466 0 0 1-.331-.136l-5.056-5.05a1.8 1.8 0 0 1-.531-1.282V1.406a.47.47 0 0 1 .468-.468h3.198Zm-1.205 3.82c.775 0 1.406-.63 1.406-1.405 0-.776-.63-1.407-1.406-1.407a1.408 1.408 0 0 0 0 2.813Zm0-1.874a.47.47 0 1 1-.001.938.47.47 0 0 1 0-.938Z"/></svg>

Before

Width:  |  Height:  |  Size: 663 B

After

Width:  |  Height:  |  Size: 660 B

View File

@ -56,14 +56,6 @@ jest.mock('./ActivityThreadList', () => {
return jest.fn().mockReturnValue(<p>ActivityThreadList</p>);
});
const mockObserve = jest.fn();
const mockunObserve = jest.fn();
window.IntersectionObserver = jest.fn().mockImplementation(() => ({
observe: mockObserve,
unobserve: mockunObserve,
}));
describe('Test ActivityThreadPanel Component', () => {
beforeAll(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -100,7 +92,5 @@ describe('Test ActivityThreadPanel Component', () => {
const obServerElement = await screen.findByTestId('observer-element');
expect(obServerElement).toBeInTheDocument();
expect(mockObserve).toHaveBeenCalled();
});
});

View File

@ -51,14 +51,6 @@ jest.mock('./ActivityThreadList', () => {
return jest.fn().mockReturnValue(<p>ActivityThreadList</p>);
});
const mockObserve = jest.fn();
const mockunObserve = jest.fn();
window.IntersectionObserver = jest.fn().mockImplementation(() => ({
observe: mockObserve,
unobserve: mockunObserve,
}));
describe('Test ActivityThreadPanelBodyBody Component', () => {
beforeAll(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -89,7 +81,5 @@ describe('Test ActivityThreadPanelBodyBody Component', () => {
const obServerElement = await findByTestId(container, 'observer-element');
expect(obServerElement).toBeInTheDocument();
expect(mockObserve).toHaveBeenCalled();
});
});

View File

@ -55,6 +55,7 @@ export const AssetSelectionModal = ({
SearchIndex.TABLE
);
const [pageNumber, setPageNumber] = useState(1);
const [totalCount, setTotalCount] = useState(0);
const fetchEntities = useCallback(
async ({ searchText = '', page = 1, index = activeFilter }) => {
@ -68,7 +69,7 @@ export const AssetSelectionModal = ({
queryFilter: getQueryFilterToExcludeTerm(glossaryFQN),
});
const hits = res.hits.hits as SearchedDataProps['data'];
setTotalCount(res.hits.total.value ?? 0);
setItems(page === 1 ? hits : (prevItems) => [...prevItems, ...hits]);
setPageNumber(page);
} catch (error) {
@ -174,7 +175,10 @@ export const AssetSelectionModal = ({
const onScroll: UIEventHandler<HTMLElement> = useCallback(
(e) => {
if (e.currentTarget.scrollHeight - e.currentTarget.scrollTop === 500) {
if (
e.currentTarget.scrollHeight - e.currentTarget.scrollTop === 500 &&
items.length < totalCount
) {
!isLoading &&
fetchEntities({
searchText: search,
@ -183,7 +187,7 @@ export const AssetSelectionModal = ({
});
}
},
[activeFilter, search]
[activeFilter, search, totalCount, items]
);
return (
@ -202,7 +206,8 @@ export const AssetSelectionModal = ({
open={open}
style={{ top: 40 }}
title={t('label.add-entity', { entity: t('label.asset-plural') })}
width={750}>
width={750}
onCancel={onCancel}>
<Space className="w-full h-full" direction="vertical" size={16}>
<Searchbar
removeMargin
@ -234,7 +239,7 @@ export const AssetSelectionModal = ({
height={500}
itemKey="id"
onScroll={onScroll}>
{({ _index: index, _source: item }) => (
{({ _source: item }) => (
<TableDataCardV2
openEntityInNewPage
showCheckboxes
@ -243,8 +248,7 @@ export const AssetSelectionModal = ({
handleSummaryPanelDisplay={handleCardClick}
id={`tabledatacard-${item.id}`}
key={item.id}
searchIndex={index}
source={item}
source={{ ...item, tags: [] }}
/>
)}
</VirtualList>

View File

@ -16,3 +16,20 @@
.asset-selection-model-card.table-data-card-container:hover {
box-shadow: 2px 3px 5px rgba(0, 0, 0, 0.13);
}
.asset-selection-model-card {
// CheckBox
.ant-checkbox-inner {
border-width: 2px;
height: 18px;
width: 18px;
border-color: #7147e8;
}
.ant-checkbox-inner::after {
top: 48%;
left: 18.5%;
width: 6.25px;
height: 10px;
}
}

View File

@ -399,6 +399,7 @@ const ContainerVersion: React.FC<ContainerVersionProp> = ({
entityName={currentVersionData.name ?? ''}
extraInfo={getExtraInfo()}
followersList={[]}
serviceType={currentVersionData.serviceType ?? ''}
tags={getTags()}
tier={undefined}
titleLinks={breadCrumbList}

View File

@ -662,6 +662,7 @@ const DashboardDetails = ({
? onRemoveTier
: undefined
}
serviceType={dashboardDetails.serviceType ?? ''}
tags={dashboardTags}
tagsHandler={onTagUpdate}
tier={tier}

View File

@ -112,14 +112,6 @@ const DashboardDetailsProps = {
onExtensionUpdate: jest.fn(),
};
const mockObserve = jest.fn();
const mockunObserve = jest.fn();
window.IntersectionObserver = jest.fn().mockImplementation(() => ({
observe: mockObserve,
unobserve: mockunObserve,
}));
jest.mock('../common/description/Description', () => {
return jest.fn().mockReturnValue(<p>Description Component</p>);
});
@ -285,8 +277,6 @@ describe('Test DashboardDetails component', () => {
const obServerElement = await findByTestId(container, 'observer-element');
expect(obServerElement).toBeInTheDocument();
expect(mockObserve).toHaveBeenCalled();
});
it('Check if tags and glossary-terms are present', async () => {

View File

@ -283,6 +283,7 @@ const DashboardVersion: FC<DashboardVersionProp> = ({
}
extraInfo={getExtraInfo()}
followersList={[]}
serviceType={currentVersionData.serviceType ?? ''}
tags={getTags()}
tier={{} as TagLabel}
titleLinks={slashedDashboardName}

View File

@ -268,6 +268,7 @@ const DataModelVersion: FC<DataModelVersionProp> = ({
}
extraInfo={getExtraInfo()}
followersList={[]}
serviceType={currentVersionData.serviceType ?? ''}
tags={getTags()}
tier={{} as TagLabel}
titleLinks={slashedDataModelName}

View File

@ -44,7 +44,6 @@ import {
} from 'utils/CommonUtils';
import { getEntityName } from 'utils/EntityUtils';
import { getEntityFieldThreadCounts } from 'utils/FeedUtils';
import { serviceTypeLogo } from 'utils/ServiceUtils';
import { getTagsWithoutTier, getTierTags } from 'utils/TableUtils';
import { DataModelDetailsProps } from './DataModelDetails.interface';
import ModelTab from './ModelTab/ModelTab.component';
@ -119,6 +118,7 @@ const DataModelDetails = ({
followers,
dataModelType,
isUserFollowing,
serviceType,
} = useMemo(() => {
return {
deleted: dataModelData?.deleted,
@ -134,11 +134,11 @@ const DataModelDetails = ({
),
followers: dataModelData?.followers ?? [],
dataModelType: dataModelData?.dataModelType,
serviceType: dataModelData?.serviceType,
};
}, [dataModelData]);
const breadcrumbTitles = useMemo(() => {
const serviceType = dataModelData?.serviceType;
const service = dataModelData?.service;
const serviceName = service?.name;
@ -151,12 +151,6 @@ const DataModelDetails = ({
ServiceCategory.DASHBOARD_SERVICES
)
: '',
imgSrc: serviceType ? serviceTypeLogo(serviceType) : undefined,
},
{
name: entityName,
url: '',
activeTitle: true,
},
];
}, [dataModelData, dashboardDataModelFQN, entityName]);
@ -247,6 +241,7 @@ const DataModelDetails = ({
isFollowing={isUserFollowing}
isTagEditable={hasEditTagsPermission}
removeTier={hasEditTierPermission ? handleRemoveTier : undefined}
serviceType={serviceType ?? ''}
tags={tags}
tagsHandler={handleUpdateTags}
tier={tier}

View File

@ -14,6 +14,9 @@
import { Card, Col, Row, Skeleton, Space, Typography } from 'antd';
import { AxiosError } from 'axios';
import classNames from 'classnames';
// css
import QueryCount from 'components/common/QueryCount/QueryCount.component';
import PageLayoutV1 from 'components/containers/PageLayoutV1';
import { isEqual, isNil, isUndefined } from 'lodash';
import { EntityTags, ExtraInfo } from 'Models';
import React, {
@ -82,8 +85,6 @@ import TableProfilerGraph from '../TableProfiler/TableProfilerGraph.component';
import TableProfilerV1 from '../TableProfiler/TableProfilerV1';
import TableQueries from '../TableQueries/TableQueries';
import { DatasetDetailsProps } from './DatasetDetails.interface';
// css
import QueryCount from 'components/common/QueryCount/QueryCount.component';
import './datasetDetails.style.less';
const DatasetDetails: React.FC<DatasetDetailsProps> = ({
@ -606,7 +607,10 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
<Loader />
) : (
<PageContainerV1>
<div className="entity-details-container">
<PageLayoutV1
pageTitle={t('label.entity-detail-plural', {
entity: getEntityName(tableDetails),
})}>
<EntityPageInfo
canDelete={tablePermissions.Delete}
currentOwner={tableDetails.owner}
@ -634,6 +638,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
? onRemoveTier
: undefined
}
serviceType={tableDetails.serviceType ?? ''}
tags={tableTags}
tagsHandler={onTagUpdate}
tier={tier}
@ -654,7 +659,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
onThreadLinkSelect={onThreadLinkSelect}
/>
<div className="tw-mt-4 tw-flex tw-flex-col tw-flex-grow">
<div className="m-t-md h-inherit">
<TabsPane
activeTab={activeTab}
className="tw-flex-initial"
@ -838,7 +843,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
/>
) : null}
</div>
</div>
</PageLayoutV1>
</PageContainerV1>
);
};

View File

@ -224,14 +224,6 @@ jest.mock('../../utils/CommonUtils', () => ({
getOwnerValue: jest.fn().mockReturnValue('Owner'),
}));
const mockObserve = jest.fn();
const mockunObserve = jest.fn();
window.IntersectionObserver = jest.fn().mockImplementation(() => ({
observe: mockObserve,
unobserve: mockunObserve,
}));
jest.mock('../PermissionProvider/PermissionProvider', () => ({
usePermissionProvider: jest.fn().mockImplementation(() => ({
permissions: {},
@ -285,6 +277,10 @@ jest.mock('../../utils/PermissionsUtils', () => ({
},
}));
jest.mock('components/containers/PageLayoutV1', () => {
return jest.fn().mockImplementation(({ children }) => children);
});
describe('Test MyDataDetailsPage page', () => {
it('Checks if the page has all the proper components rendered', async () => {
const { container } = render(<DatasetDetails {...DatasetDetailsProps} />, {
@ -417,7 +413,5 @@ describe('Test MyDataDetailsPage page', () => {
const obServerElement = await findByTestId(container, 'observer-element');
expect(obServerElement).toBeInTheDocument();
expect(mockObserve).toHaveBeenCalled();
});
});

View File

@ -390,10 +390,11 @@ const DatasetVersion: React.FC<DatasetVersionProp> = ({
entityName={currentVersionData.name ?? ''}
extraInfo={getExtraInfo()}
followersList={[]}
serviceType={currentVersionData.serviceType ?? ''}
tags={getTags()}
tier={tier}
titleLinks={slashedTableName}
version={version}
version={Number(version)}
versionHandler={backHandler}
/>
<div className="tw-mt-1 tw-flex tw-flex-col tw-flex-grow ">

View File

@ -0,0 +1,77 @@
/*
* 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 { Col, Row } from 'antd';
import classNames from 'classnames';
import TitleBreadcrumb from 'components/common/title-breadcrumb/title-breadcrumb.component';
import { TitleBreadcrumbProps } from 'components/common/title-breadcrumb/title-breadcrumb.interface';
import { EntityType } from 'enums/entity.enum';
import React, { ReactNode } from 'react';
import { getEntityLinkFromType, getEntityName } from 'utils/EntityUtils';
import EntityHeaderTitle from '../EntityHeaderTitle/EntityHeaderTitle.component';
interface Props {
extra?: ReactNode;
breadcrumb: TitleBreadcrumbProps['titleLinks'];
entityData: {
displayName?: string;
name: string;
fullyQualifiedName?: string;
deleted?: boolean;
};
entityType?: EntityType;
icon: ReactNode;
titleIsLink?: boolean;
openEntityInNewPage?: boolean;
gutter?: 'default' | 'large';
}
export const EntityHeader = ({
breadcrumb,
entityData,
extra,
icon,
titleIsLink = false,
entityType,
openEntityInNewPage,
gutter = 'default',
}: Props) => {
return (
<Row className="w-full" gutter={0} justify="space-between">
<Col>
<div
className={classNames(
'tw-text-link tw-text-base glossary-breadcrumb',
gutter === 'large' ? 'm-b-sm' : 'm-b-xss'
)}
data-testid="category-name">
<TitleBreadcrumb titleLinks={breadcrumb} />
</div>
<EntityHeaderTitle
deleted={entityData.deleted}
displayName={getEntityName(entityData)}
icon={icon}
link={
titleIsLink && entityData.fullyQualifiedName && entityType
? getEntityLinkFromType(entityData.fullyQualifiedName, entityType)
: undefined
}
name={entityData.name}
openEntityInNewPage={openEntityInNewPage}
/>
</Col>
<Col>{extra}</Col>
</Row>
);
};

View File

@ -0,0 +1,80 @@
/*
* 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 { ExclamationCircleFilled } from '@ant-design/icons';
import { Col, Row, Typography } from 'antd';
import { ReactComponent as IconExternalLink } from 'assets/svg/external-link-grey.svg';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { EntityHeaderTitleProps } from './EntityHeaderTitle.interface';
const EntityHeaderTitle = ({
icon,
name,
displayName,
link,
openEntityInNewPage,
deleted = false,
}: EntityHeaderTitleProps) => {
const { t } = useTranslation();
return (
<Row align="middle" gutter={8} wrap={false}>
<Col>{icon}</Col>
<Col>
<div>
<Typography.Text
className="m-b-0 d-block tw-text-xs tw-text-grey-muted"
data-testid="entity-header-name">
{name}
</Typography.Text>
{link ? (
<Link
className="m-b-0 d-block entity-header-display-name text-lg font-bold"
data-testid="entity-header-display-name"
target={openEntityInNewPage ? '_blank' : '_self'}
to={link}>
<Typography.Text ellipsis={{ tooltip: true }}>
{displayName ?? name}
{openEntityInNewPage && (
<IconExternalLink
className="anticon vertical-baseline m-l-xss"
height={14}
width={14}
/>
)}
</Typography.Text>
</Link>
) : (
<Typography.Text
className="m-b-0 d-block entity-header-display-name text-lg font-bold"
data-testid="entity-header-display-name"
ellipsis={{ tooltip: true }}>
{displayName ?? name}
</Typography.Text>
)}
</div>
</Col>
{deleted && (
<Col className="self-end text-xs">
<div className="deleted-badge-button" data-testid="deleted-badge">
<ExclamationCircleFilled className="m-r-xss font-medium text-xs" />
{t('label.deleted')}
</div>
</Col>
)}
</Row>
);
};
export default EntityHeaderTitle;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2022 Collate.
* 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
@ -10,25 +10,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import url('../../../styles/variables.less');
@link-btn-color: #37352f;
.table-data-card-title-container {
.ant-btn-link {
color: @link-btn-color;
font-weight: 600;
padding: 0px;
font-size: 16px;
}
.ant-btn-link > span {
color: @link-btn-color;
}
}
.button-hover {
.ant-btn-link > span {
&:hover {
color: @primary-color;
}
}
export interface EntityHeaderTitleProps {
icon: React.ReactNode;
name: string;
displayName: string;
link?: string;
openEntityInNewPage?: boolean;
deleted?: boolean;
}

View File

@ -0,0 +1,71 @@
/*
* 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, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import EntityHeaderTitle from './EntityHeaderTitle.component';
describe('EntityHeaderTitle', () => {
it('should render icon', () => {
render(
<EntityHeaderTitle
displayName="Test DisplayName"
icon="test-icon"
name="test-name"
/>
);
expect(screen.getByText('test-icon')).toBeInTheDocument();
});
it('should render name', () => {
render(
<EntityHeaderTitle
displayName="Test DisplayName"
icon="test-icon"
name="test-name"
/>
);
expect(screen.getByText('test-name')).toBeInTheDocument();
});
it('should render displayName', () => {
render(
<EntityHeaderTitle
displayName="Test DisplayName"
icon="test-icon"
name="test-name"
/>
);
expect(screen.getByText('Test DisplayName')).toBeInTheDocument();
});
it('should render link if link is provided', () => {
render(
<EntityHeaderTitle
displayName="Test DisplayName"
icon="test-icon"
link="test-link"
name="test-name"
/>,
{ wrapper: MemoryRouter }
);
expect(screen.getByTestId('entity-header-display-name')).toHaveProperty(
'href',
'http://localhost/test-link'
);
});
});

View File

@ -1,45 +0,0 @@
/*
* 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 { Col, Row, Typography } from 'antd';
import React from 'react';
interface props {
icon: React.ReactNode;
name: string;
displayName: string;
}
const EntityHeaderTitle = ({ icon, name, displayName }: props) => {
return (
<Row align="middle" gutter={8} wrap={false}>
<Col>{icon}</Col>
<Col>
<div>
<Typography.Text
className="m-b-0 d-block tw-text-xs tw-text-grey-muted"
data-testid="entity-header-name">
{name}
</Typography.Text>
<Typography.Text
className="m-b-0 d-block entity-header-display-name text-lg font-bold"
data-testid="entity-header-display-name"
ellipsis={{ tooltip: true }}>
{displayName}
</Typography.Text>
</div>
</Col>
</Row>
);
};
export default EntityHeaderTitle;

View File

@ -12,19 +12,21 @@
*/
import { CloseOutlined } from '@ant-design/icons';
import { Col, Drawer, Row } from 'antd';
import TableDataCardTitle from 'components/common/table-data-card-v2/TableDataCardTitle.component';
import { Drawer } from 'antd';
import { EntityHeader } from 'components/Entity/EntityHeader/EntityHeader.component';
import { EntityType } from 'enums/entity.enum';
import { Tag } from 'generated/entity/classification/tag';
import { Container } from 'generated/entity/data/container';
import { Dashboard } from 'generated/entity/data/dashboard';
import { GlossaryTerm } from 'generated/entity/data/glossaryTerm';
import { Table } from 'generated/entity/data/table';
import { get } from 'lodash';
import React, { useMemo, useState } from 'react';
import React, { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { Dashboard } from '../../../generated/entity/data/dashboard';
import { getEntityBreadcrumbs } from 'utils/EntityUtils';
import { getServiceIcon } from 'utils/TableUtils';
import { Mlmodel } from '../../../generated/entity/data/mlmodel';
import { Pipeline } from '../../../generated/entity/data/pipeline';
import { Table } from '../../../generated/entity/data/table';
import { Topic } from '../../../generated/entity/data/topic';
import ContainerSummary from './ContainerSummary/ContainerSummary.component';
import DashboardSummary from './DashboardSummary/DashboardSummary.component';
@ -42,70 +44,44 @@ export default function EntitySummaryPanel({
handleClosePanel,
}: EntitySummaryPanelProps) {
const { tab } = useParams<{ tab: string }>();
const [currentSearchIndex, setCurrentSearchIndex] = useState<EntityType>();
const summaryComponent = useMemo(() => {
const type = get(entityDetails, 'details.entityType') ?? EntityType.TABLE;
const entity = entityDetails.details;
switch (type) {
case EntityType.TABLE:
setCurrentSearchIndex(EntityType.TABLE);
return <TableSummary entityDetails={entityDetails.details as Table} />;
return <TableSummary entityDetails={entity as Table} />;
case EntityType.TOPIC:
setCurrentSearchIndex(EntityType.TOPIC);
return <TopicSummary entityDetails={entityDetails.details as Topic} />;
return <TopicSummary entityDetails={entity as Topic} />;
case EntityType.DASHBOARD:
setCurrentSearchIndex(EntityType.DASHBOARD);
return (
<DashboardSummary
entityDetails={entityDetails.details as Dashboard}
/>
);
return <DashboardSummary entityDetails={entity as Dashboard} />;
case EntityType.PIPELINE:
setCurrentSearchIndex(EntityType.PIPELINE);
return (
<PipelineSummary entityDetails={entityDetails.details as Pipeline} />
);
return <PipelineSummary entityDetails={entity as Pipeline} />;
case EntityType.MLMODEL:
setCurrentSearchIndex(EntityType.MLMODEL);
return (
<MlModelSummary entityDetails={entityDetails.details as Mlmodel} />
);
return <MlModelSummary entityDetails={entity as Mlmodel} />;
case EntityType.CONTAINER:
setCurrentSearchIndex(EntityType.CONTAINER);
return <ContainerSummary entityDetails={entity as Container} />;
return (
<ContainerSummary
entityDetails={entityDetails.details as Container}
/>
);
case EntityType.GLOSSARY_TERM:
setCurrentSearchIndex(EntityType.GLOSSARY);
return <GlossaryTermSummary entityDetails={entity as GlossaryTerm} />;
return (
<GlossaryTermSummary
entityDetails={entityDetails.details as GlossaryTerm}
/>
);
case EntityType.TAG:
setCurrentSearchIndex(EntityType.TAG);
return <TagsSummary entityDetails={entityDetails.details as Tag} />;
return <TagsSummary entityDetails={entity as Tag} />;
default:
return null;
}
}, [tab, entityDetails]);
const icon = useMemo(() => {
return getServiceIcon(entityDetails.details);
}, [entityDetails]);
return (
<Drawer
destroyOnClose
@ -122,16 +98,16 @@ export default function EntitySummaryPanel({
headerStyle={{ padding: 16 }}
mask={false}
title={
<Row gutter={[0, 6]}>
<Col span={24}>
<TableDataCardTitle
isPanel
dataTestId="summary-panel-title"
searchIndex={currentSearchIndex as EntityType}
source={entityDetails.details}
/>
</Col>
</Row>
<EntityHeader
titleIsLink
breadcrumb={getEntityBreadcrumbs(
entityDetails.details,
entityDetails.details.entityType as EntityType
)}
entityData={entityDetails.details}
entityType={entityDetails.details.entityType as EntityType}
icon={icon}
/>
}
width="100%">
{summaryComponent}

View File

@ -64,36 +64,30 @@ jest.mock('./MlModelSummary/MlModelSummary.component', () =>
))
);
jest.mock(
'components/common/table-data-card-v2/TableDataCardTitle.component',
() =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="table-data-card-title">TableDataCardTitle</div>
))
);
jest.mock('react-router-dom', () => ({
useParams: jest.fn().mockImplementation(() => ({ tab: 'table' })),
}));
jest.mock('components/Entity/EntityHeader/EntityHeader.component', () => ({
EntityHeader: jest.fn().mockImplementation(() => <p>EntityHeader</p>),
}));
describe('EntitySummaryPanel component tests', () => {
it('TableSummary should render for table data', async () => {
render(
<EntitySummaryPanel
entityDetails={{
details: mockTableEntityDetails,
details: { ...mockTableEntityDetails, entityType: EntityType.TABLE },
}}
handleClosePanel={mockHandleClosePanel}
/>
);
const tableDataCardTitle = screen.getByText('TableDataCardTitle');
const entityHeader = screen.getByText('EntityHeader');
const tableSummary = screen.getByTestId('TableSummary');
const closeIcon = screen.getByTestId('summary-panel-close-icon');
expect(tableDataCardTitle).toBeInTheDocument();
expect(entityHeader).toBeInTheDocument();
expect(tableSummary).toBeInTheDocument();
expect(closeIcon).toBeInTheDocument();

View File

@ -45,7 +45,7 @@ function SummaryListItem({
{entityDetails.title}
</Col>
<Col span={24}>
<Row className="text-xs font-300" gutter={[4, 4]}>
<Row className="text-xs font-thin" gutter={[4, 4]}>
<Col>
{entityDetails.type && (
<Space size={4}>

View File

@ -18,7 +18,6 @@ import { EntityUnion } from 'components/Explore/explore.interface';
import { SourceType } from 'components/searched-data/SearchedData.interface';
import SummaryPanelSkeleton from 'components/Skeleton/SummaryPanelSkeleton/SummaryPanelSkeleton.component';
import { SearchIndex } from 'enums/search.enum';
import { get } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { searchData } from 'rest/miscAPI';
@ -50,14 +49,11 @@ function TagsSummary({ entityDetails, isLoading }: TagsSummaryProps) {
const usageItems = useMemo(() => {
return selectedData.map((entity, index) => {
const searchIndex = get(entity, 'entityType');
return (
<>
<div className="mb-2">
<TableDataCardV2
id={`tabledatacardtest${index}`}
searchIndex={searchIndex}
source={entity as SourceType}
/>
</div>

View File

@ -127,6 +127,13 @@ export type EntityUnion =
| Tag
| DashboardDataModel;
export type EntityWithServices =
| Topic
| Dashboard
| Pipeline
| Mlmodel
| Container;
export interface EntityDetailsObjectInterface {
details: SearchedDataProps['data'][number]['_source'];
}

View File

@ -13,16 +13,15 @@
import { Col, Row } from 'antd';
import { ReactComponent as IconFolder } from 'assets/svg/folder.svg';
import { ReactComponent as IconFlatDoc } from 'assets/svg/ic-flat-doc.svg';
import TitleBreadcrumb from 'components/common/title-breadcrumb/title-breadcrumb.component';
import { TitleBreadcrumbProps } from 'components/common/title-breadcrumb/title-breadcrumb.interface';
import EntityHeaderTitle from 'components/EntityHeaderTitle/EntityHeaderTitle.component';
import { EntityHeader } from 'components/Entity/EntityHeader/EntityHeader.component';
import { OperationPermission } from 'components/PermissionProvider/PermissionProvider.interface';
import { FQN_SEPARATOR_CHAR } from 'constants/char.constants';
import { DE_ACTIVE_COLOR } from 'constants/constants';
import { EntityType } from 'enums/entity.enum';
import { Glossary } from 'generated/entity/data/glossary';
import { GlossaryTerm } from 'generated/entity/data/glossaryTerm';
import React, { useEffect, useState } from 'react';
import { getEntityName } from 'utils/EntityUtils';
import { getGlossaryPath } from 'utils/RouterUtils';
import GlossaryHeaderButtons from '../GlossaryHeaderButtons/GlossaryHeaderButtons.component';
@ -33,7 +32,7 @@ export interface GlossaryHeaderProps {
isGlossary: boolean;
onUpdate: (data: GlossaryTerm | Glossary) => void;
onDelete: (id: string) => void;
onAssetsUpdate?: () => void;
onAssetAdd?: () => void;
}
const GlossaryHeader = ({
@ -42,7 +41,7 @@ const GlossaryHeader = ({
onUpdate,
onDelete,
isGlossary,
onAssetsUpdate,
onAssetAdd,
}: GlossaryHeaderProps) => {
const [breadcrumb, setBreadcrumb] = useState<
TitleBreadcrumbProps['titleLinks']
@ -88,50 +87,42 @@ const GlossaryHeader = ({
<>
<Row gutter={[0, 16]}>
<Col span={24}>
<Row justify="space-between">
<Col span={12}>
<div
className="tw-text-link tw-text-base glossary-breadcrumb m-b-sm"
data-testid="category-name">
<TitleBreadcrumb titleLinks={breadcrumb} />
</div>
<EntityHeaderTitle
displayName={getEntityName(selectedData)}
icon={
isGlossary ? (
<IconFolder
color={DE_ACTIVE_COLOR}
height={36}
name="folder"
width={32}
/>
) : (
<IconFlatDoc
color={DE_ACTIVE_COLOR}
height={36}
name="doc"
width={32}
/>
)
}
name={selectedData.name}
/>
</Col>
<Col span={12}>
<EntityHeader
breadcrumb={breadcrumb}
entityData={selectedData}
entityType={EntityType.GLOSSARY_TERM}
extra={
<div style={{ textAlign: 'right' }}>
<GlossaryHeaderButtons
deleteStatus="success"
isGlossary={isGlossary}
permission={permissions}
selectedData={selectedData}
onAssetsUpdate={onAssetsUpdate}
onAssetAdd={onAssetAdd}
onEntityDelete={onDelete}
onUpdate={onUpdate}
/>
</div>
</Col>
</Row>
}
gutter="large"
icon={
isGlossary ? (
<IconFolder
color={DE_ACTIVE_COLOR}
height={36}
name="folder"
width={32}
/>
) : (
<IconFlatDoc
color={DE_ACTIVE_COLOR}
height={36}
name="doc"
width={32}
/>
)
}
/>
</Col>
</Row>
</>

View File

@ -16,7 +16,6 @@ import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import { ReactComponent as ExportIcon } from 'assets/svg/ic-export.svg';
import { ReactComponent as ImportIcon } from 'assets/svg/ic-import.svg';
import { ReactComponent as IconDropdown } from 'assets/svg/menu.svg';
import { AssetSelectionModal } from 'components/Assets/AssetsSelectionModal/AssetSelectionModal';
import EntityDeleteModal from 'components/Modals/EntityDeleteModal/EntityDeleteModal';
import EntityNameModal from 'components/Modals/EntityNameModal/EntityNameModal.component';
import { OperationPermission } from 'components/PermissionProvider/PermissionProvider.interface';
@ -48,7 +47,7 @@ interface GlossaryHeaderButtonsProps {
selectedData: Glossary | GlossaryTerm;
permission: OperationPermission;
onEntityDelete: (id: string) => void;
onAssetsUpdate?: () => void;
onAssetAdd?: () => void;
onUpdate: (data: GlossaryTerm | Glossary) => void;
}
@ -58,7 +57,7 @@ const GlossaryHeaderButtons = ({
selectedData,
permission,
onEntityDelete,
onAssetsUpdate,
onAssetAdd,
onUpdate,
}: GlossaryHeaderButtonsProps) => {
const { t } = useTranslation();
@ -75,7 +74,7 @@ const GlossaryHeaderButtons = ({
const history = useHistory();
const [showActions, setShowActions] = useState(false);
const [isDelete, setIsDelete] = useState<boolean>(false);
const [showAddAssets, setShowAddAssets] = useState(false);
const [isNameEditing, setIsNameEditing] = useState<boolean>(false);
const editDisplayNamePermission = useMemo(() => {
@ -133,10 +132,6 @@ const GlossaryHeaderButtons = ({
setIsDelete(false);
};
const handleAddAssetsClick = () => {
setShowAddAssets(true);
};
const onNameSave = (obj: { name: string; displayName: string }) => {
const { name, displayName } = obj;
let updatedDetails = cloneDeep(selectedData);
@ -161,7 +156,7 @@ const GlossaryHeaderButtons = ({
{
label: t('label.asset-plural'),
key: '2',
onClick: () => handleAddAssetsClick(),
onClick: onAssetAdd,
},
];
@ -388,14 +383,7 @@ const GlossaryHeaderButtons = ({
onOk={handleCancelGlossaryExport}
/>
)}
{selectedData.fullyQualifiedName && !isGlossary && (
<AssetSelectionModal
glossaryFQN={selectedData.fullyQualifiedName}
open={showAddAssets}
onCancel={() => setShowAddAssets(false)}
onSave={onAssetsUpdate}
/>
)}
<EntityNameModal
entity={selectedData as EntityReference}
visible={isNameEditing}

View File

@ -19,12 +19,10 @@ import AssetsTabs, {
import GlossaryOverviewTab from 'components/GlossaryTerms/tabs/GlossaryOverviewTab.component';
import { OperationPermission } from 'components/PermissionProvider/PermissionProvider.interface';
import { getGlossaryTermDetailsPath } from 'constants/constants';
import { myDataSearchIndex } from 'constants/Mydata.constants';
import { Glossary } from 'generated/entity/data/glossary';
import { t } from 'i18next';
import React, { RefObject, useEffect, useMemo, useState } from 'react';
import React, { RefObject, useMemo } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { searchData } from 'rest/miscAPI';
import { getGlossaryTermsVersionsPath } from 'utils/RouterUtils';
import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm';
import { getCountBadge } from '../../utils/CommonUtils';
@ -34,12 +32,13 @@ type Props = {
childGlossaryTerms: GlossaryTerm[];
permissions: OperationPermission;
isGlossary: boolean;
onAddAsset: () => void;
onUpdate: (data: GlossaryTerm | Glossary) => void;
refreshGlossaryTerms: () => void;
onAssetClick?: (asset?: EntityDetailsObjectInterface) => void;
assetsRef: RefObject<AssetsTabRef>;
isSummaryPanelOpen: boolean;
assetCount?: number;
};
const GlossaryTabs = ({
@ -47,10 +46,12 @@ const GlossaryTabs = ({
childGlossaryTerms,
isGlossary,
onUpdate,
onAddAsset,
permissions,
refreshGlossaryTerms,
onAssetClick,
assetsRef,
assetCount,
isSummaryPanelOpen,
}: Props) => {
const {
@ -59,7 +60,6 @@ const GlossaryTabs = ({
version,
} = useParams<{ glossaryName: string; tab: string; version: string }>();
const history = useHistory();
const [assetCount, setAssetCount] = useState<number>(0);
const activeTabHandler = (tab: string) => {
history.push({
@ -69,30 +69,6 @@ const GlossaryTabs = ({
});
};
const fetchGlossaryTermAssets = async () => {
if (glossaryFqn) {
try {
const res = await searchData(
'',
1,
0,
`(tags.tagFQN:"${glossaryFqn}")`,
'',
'',
myDataSearchIndex
);
setAssetCount(res.data.hits.total.value ?? 0);
} catch (error) {
setAssetCount(0);
}
}
};
useEffect(() => {
fetchGlossaryTermAssets();
}, [glossaryFqn]);
const activeTab = useMemo(() => {
return tab ?? 'overview';
}, [tab]);
@ -144,7 +120,7 @@ const GlossaryTabs = ({
<div data-testid="assets">
{t('label.asset-plural')}
<span className="p-l-xs ">
{getCountBadge(assetCount, '', activeTab === 'assets')}
{getCountBadge(assetCount ?? 0, '', activeTab === 'assets')}
</span>
</div>
),
@ -154,6 +130,7 @@ const GlossaryTabs = ({
isSummaryPanelOpen={isSummaryPanelOpen}
permissions={permissions}
ref={assetsRef}
onAddAsset={onAddAsset}
onAssetClick={onAssetClick}
/>
),

View File

@ -12,10 +12,14 @@
*/
import { Col, Row } from 'antd';
import { AssetSelectionModal } from 'components/Assets/AssetsSelectionModal/AssetSelectionModal';
import { EntityDetailsObjectInterface } from 'components/Explore/explore.interface';
import GlossaryHeader from 'components/Glossary/GlossaryHeader/GlossaryHeader.component';
import GlossaryTabs from 'components/GlossaryTabs/GlossaryTabs.component';
import React, { useRef } from 'react';
import { myDataSearchIndex } from 'constants/Mydata.constants';
import React, { useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import { searchData } from 'rest/miscAPI';
import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm';
import { OperationPermission } from '../PermissionProvider/PermissionProvider.interface';
import { AssetsTabRef } from './tabs/AssetsTabs.component';
@ -41,39 +45,80 @@ const GlossaryTermsV1 = ({
onAssetClick,
isSummaryPanelOpen,
}: Props) => {
const { glossaryName: glossaryFqn } = useParams<{ glossaryName: string }>();
const assetTabRef = useRef<AssetsTabRef>(null);
const [assetModalVisible, setAssetModelVisible] = useState(false);
const [assetCount, setAssetCount] = useState<number>(0);
const fetchGlossaryTermAssets = async () => {
if (glossaryFqn) {
try {
const res = await searchData(
'',
1,
0,
`(tags.tagFQN:"${glossaryFqn}")`,
'',
'',
myDataSearchIndex
);
setAssetCount(res.data.hits.total.value ?? 0);
} catch (error) {
setAssetCount(0);
}
}
};
useEffect(() => {
fetchGlossaryTermAssets();
}, [glossaryFqn]);
const handleAssetSave = () => {
fetchGlossaryTermAssets();
assetTabRef.current?.refreshAssets();
};
return (
<Row data-testid="glossary-term" gutter={[0, 8]}>
<Col span={24}>
<GlossaryHeader
isGlossary={false}
permissions={permissions}
selectedData={glossaryTerm}
onAssetsUpdate={() => {
if (glossaryTerm.fullyQualifiedName) {
assetTabRef.current?.refreshAssets();
}
}}
onDelete={handleGlossaryTermDelete}
onUpdate={(data) => handleGlossaryTermUpdate(data as GlossaryTerm)}
/>
</Col>
<>
<Row data-testid="glossary-term" gutter={[0, 8]}>
<Col span={24}>
<GlossaryHeader
isGlossary={false}
permissions={permissions}
selectedData={glossaryTerm}
onAssetAdd={() => setAssetModelVisible(true)}
onDelete={handleGlossaryTermDelete}
onUpdate={(data) => handleGlossaryTermUpdate(data as GlossaryTerm)}
/>
</Col>
<Col span={24}>
<GlossaryTabs
assetsRef={assetTabRef}
childGlossaryTerms={childGlossaryTerms}
isGlossary={false}
isSummaryPanelOpen={isSummaryPanelOpen}
permissions={permissions}
refreshGlossaryTerms={refreshGlossaryTerms}
selectedData={glossaryTerm}
onAssetClick={onAssetClick}
onUpdate={(data) => handleGlossaryTermUpdate(data as GlossaryTerm)}
<Col span={24}>
<GlossaryTabs
assetCount={assetCount}
assetsRef={assetTabRef}
childGlossaryTerms={childGlossaryTerms}
isGlossary={false}
isSummaryPanelOpen={isSummaryPanelOpen}
permissions={permissions}
refreshGlossaryTerms={refreshGlossaryTerms}
selectedData={glossaryTerm}
onAddAsset={() => setAssetModelVisible(true)}
onAssetClick={onAssetClick}
onUpdate={(data) => handleGlossaryTermUpdate(data as GlossaryTerm)}
/>
</Col>
</Row>
{glossaryTerm.fullyQualifiedName && (
<AssetSelectionModal
glossaryFQN={glossaryTerm.fullyQualifiedName}
open={assetModalVisible}
onCancel={() => setAssetModelVisible(false)}
onSave={handleAssetSave}
/>
</Col>
</Row>
)}
</>
);
};

View File

@ -31,7 +31,7 @@ import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
import { EntityType } from 'enums/entity.enum';
import { SearchIndex } from 'enums/search.enum';
import { t } from 'i18next';
import { startCase } from 'lodash';
import { find, startCase } from 'lodash';
import React, {
forwardRef,
useCallback,
@ -46,6 +46,7 @@ import { getCountBadge } from '../../../utils/CommonUtils';
import ErrorPlaceHolder from '../../common/error-with-placeholder/ErrorPlaceHolder';
interface Props {
onAddAsset: () => void;
permissions: OperationPermission;
onAssetClick?: (asset?: EntityDetailsObjectInterface) => void;
isSummaryPanelOpen: boolean;
@ -57,7 +58,10 @@ export interface AssetsTabRef {
}
const AssetsTabs = forwardRef(
({ permissions, onAssetClick, isSummaryPanelOpen }: Props, ref) => {
(
{ permissions, onAssetClick, isSummaryPanelOpen, onAddAsset }: Props,
ref
) => {
const [itemCount, setItemCount] = useState<Record<AssetsUnion, number>>({
table: 0,
pipeline: 0,
@ -98,22 +102,32 @@ const AssetsTabs = forwardRef(
mlmodelResponse,
containerResponse,
]) => {
setItemCount({
const counts = {
[EntityType.TOPIC]: topicResponse.data.hits.total.value,
[EntityType.TABLE]: tableResponse.data.hits.total.value,
[EntityType.DASHBOARD]: dashboardResponse.data.hits.total.value,
[EntityType.PIPELINE]: pipelineResponse.data.hits.total.value,
[EntityType.MLMODEL]: mlmodelResponse.data.hits.total.value,
[EntityType.CONTAINER]: containerResponse.data.hits.total.value,
});
};
setItemCount(counts);
setActiveFilter(
tableResponse.data.hits.total.value
? SearchIndex.TABLE
: topicResponse.data.hits.total.value
? SearchIndex.TOPIC
: SearchIndex.DASHBOARD
);
find(counts, (count, key) => {
if (count > 0) {
key;
const option = AssetsFilterOptions.find(
(el) => el.label === key
);
if (option) {
setActiveFilter(option.value);
}
return true;
}
return false;
});
}
)
.catch((err) => {
@ -236,7 +250,7 @@ const AssetsTabs = forwardRef(
})}
{data.length ? (
<>
{data.map(({ _source, _index, _id = '' }, index) => (
{data.map(({ _source, _id = '' }, index) => (
<TableDataCardV2
className={classNames(
'm-b-sm cursor-pointer',
@ -245,7 +259,6 @@ const AssetsTabs = forwardRef(
handleSummaryPanelDisplay={setSelectedCard}
id={_id}
key={index}
searchIndex={_index as SearchIndex}
source={_source}
/>
))}
@ -271,7 +284,8 @@ const AssetsTabs = forwardRef(
<Button
ghost
data-testid="add-new-asset-button"
type="primary">
type="primary"
onClick={onAddAsset}>
{t('label.add-entity', {
entity: t('label.asset'),
})}

View File

@ -56,7 +56,6 @@ import {
} from '../../utils/CommonUtils';
import { getEntityFieldThreadCounts } from '../../utils/FeedUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import { serviceTypeLogo } from '../../utils/ServiceUtils';
import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils';
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
import ActivityFeedList from '../ActivityFeed/ActivityFeedList/ActivityFeedList';
@ -153,6 +152,7 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
const mlModelTags = useMemo(() => {
return getTagsWithoutTier(mlModelDetail.tags || []);
}, [mlModelDetail.tags]);
const slashedMlModelName: TitleBreadcrumbProps['titleLinks'] = [
{
name: mlModelDetail.service.name || '',
@ -162,14 +162,6 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
ServiceCategory.ML_MODEL_SERVICES
)
: '',
imgSrc: mlModelDetail.serviceType
? serviceTypeLogo(mlModelDetail.serviceType || '')
: undefined,
},
{
name: getEntityName(mlModelDetail as unknown as EntityReference),
url: '',
activeTitle: true,
},
];
@ -553,6 +545,7 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
? onTierRemove
: undefined
}
serviceType={mlModelDetail.serviceType ?? ''}
tags={mlModelTags}
tagsHandler={onTagUpdate}
tier={mlModelTier}

View File

@ -273,6 +273,7 @@ const MlModelVersion: FC<MlModelVersionProp> = ({
}
extraInfo={getExtraInfo()}
followersList={[]}
serviceType={currentVersionData.serviceType ?? ''}
tags={getTags()}
tier={{} as TagLabel}
titleLinks={slashedMlModelName}

View File

@ -160,14 +160,6 @@ const mockProp: MyDataProps = {
updateThreadHandler: jest.fn(),
};
const mockObserve = jest.fn();
const mockunObserve = jest.fn();
window.IntersectionObserver = jest.fn().mockImplementation(() => ({
observe: mockObserve,
unobserve: mockunObserve,
}));
describe('Test MyData page', () => {
it('Check if there is an element in the page', async () => {
const { container } = render(<MyData {...mockProp} />, {
@ -197,8 +189,6 @@ describe('Test MyData page', () => {
const obServerElement = await findByTestId(container, 'observer-element');
expect(obServerElement).toBeInTheDocument();
expect(mockObserve).toHaveBeenCalled();
});
it('Onboarding placeholder should be visible in case of no activity feed present overall and the feeds are not loading', async () => {

View File

@ -763,6 +763,7 @@ const PipelineDetails = ({
? onTierRemove
: undefined
}
serviceType={pipelineDetails.serviceType ?? ''}
tags={tags}
tagsHandler={onTagUpdate}
tier={tier}

View File

@ -20,7 +20,6 @@ import {
screen,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { act } from 'react-test-renderer';
@ -141,14 +140,6 @@ const PipelineDetailsProps = {
onExtensionUpdate: jest.fn(),
};
const mockObserve = jest.fn();
const mockunObserve = jest.fn();
window.IntersectionObserver = jest.fn().mockImplementation(() => ({
observe: mockObserve,
unobserve: mockunObserve,
}));
jest.mock('../common/description/Description', () => {
return jest.fn().mockReturnValue(<p>Description Component</p>);
});

View File

@ -278,6 +278,7 @@ const PipelineVersion: FC<PipelineVersionProp> = ({
}
extraInfo={getExtraInfo()}
followersList={[]}
serviceType={currentVersionData.serviceType ?? ''}
tags={getTags()}
tier={{} as TagLabel}
titleLinks={slashedPipelineName}

View File

@ -466,6 +466,7 @@ const ProfilerDashboard: React.FC<ProfilerDashboardProps> = ({
? handleTierRemove
: undefined
}
serviceType={table.serviceType ?? ''}
tags={getTagsWithoutTier(table.tags || [])}
tagsHandler={handleTagUpdate}
tier={tier}

View File

@ -892,12 +892,11 @@ const TeamDetailsV1 = ({
return (
<div data-testid="table-container">
{assets.data.map(({ _source, _index, _id = '' }, index) => (
{assets.data.map(({ _source, _id = '' }, index) => (
<TableDataCardV2
className="m-b-sm cursor-pointer"
id={_id}
key={index}
searchIndex={_index as SearchIndex}
source={_source}
/>
))}

View File

@ -452,6 +452,7 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
? onTierRemove
: undefined
}
serviceType={topicDetails.serviceType ?? ''}
tags={topicTags}
tagsHandler={onTagUpdate}
tier={tier}

View File

@ -109,14 +109,6 @@ const TopicDetailsProps = {
onExtensionUpdate: jest.fn(),
};
const mockObserve = jest.fn();
const mockunObserve = jest.fn();
window.IntersectionObserver = jest.fn().mockImplementation(() => ({
observe: mockObserve,
unobserve: mockunObserve,
}));
jest.mock('../EntityLineage/EntityLineage.component', () => {
return jest.fn().mockReturnValue(<p>EntityLineage.component</p>);
});
@ -285,7 +277,5 @@ describe('Test TopicDetails component', () => {
const obServerElement = await findByTestId(container, 'observer-element');
expect(obServerElement).toBeInTheDocument();
expect(mockObserve).toHaveBeenCalled();
});
});

View File

@ -273,6 +273,7 @@ const TopicVersion: FC<TopicVersionProp> = ({
entityName={currentVersionData.name ?? ''}
extraInfo={getExtraInfo()}
followersList={[]}
serviceType={currentVersionData.serviceType ?? ''}
tags={getTags()}
tier={{} as TagLabel}
titleLinks={slashedTopicName}

View File

@ -25,7 +25,6 @@ import { ReactComponent as IconTeamsGrey } from 'assets/svg/teams-grey.svg';
import { AxiosError } from 'axios';
import TableDataCardV2 from 'components/common/table-data-card-v2/TableDataCardV2';
import TeamsSelectable from 'components/TeamsSelectable/TeamsSelectable';
import { SearchIndex } from 'enums/search.enum';
import { capitalize, isEmpty, isEqual, toLower } from 'lodash';
import { observer } from 'mobx-react';
import React, {
@ -848,12 +847,11 @@ const Users = ({
<div data-testid="table-container">
{entityData.data.length ? (
<>
{entityData.data.map(({ _source, _index, _id = '' }, index) => (
{entityData.data.map(({ _source, _id = '' }, index) => (
<TableDataCardV2
className="m-b-sm cursor-pointer"
id={_id}
key={index}
searchIndex={_index as SearchIndex}
source={_source}
/>
))}

View File

@ -114,9 +114,11 @@ const mockEntityInfoProp = {
versionHandler,
entityFieldThreads: [],
onThreadLinkSelect,
serviceType: '',
};
jest.mock('../../../utils/EntityUtils', () => ({
...jest.requireActual('../../../utils/EntityUtils'),
getEntityFeedLink: jest.fn(),
}));

View File

@ -11,11 +11,12 @@
* limitations under the License.
*/
import { ExclamationCircleOutlined, StarFilled } from '@ant-design/icons';
import { StarFilled } from '@ant-design/icons';
import { Button, Popover, Space, Tooltip } from 'antd';
import { ItemType } from 'antd/lib/menu/hooks/useItems';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { EntityHeader } from 'components/Entity/EntityHeader/EntityHeader.component';
import Tags from 'components/Tag/Tags/tags';
import { t } from 'i18next';
import { cloneDeep, isEmpty, isUndefined, toString } from 'lodash';
@ -23,6 +24,7 @@ import { EntityTags, ExtraInfo, TagOption } from 'Models';
import React, { Fragment, useCallback, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { getActiveAnnouncement } from 'rest/feedsAPI';
import { serviceTypeLogo } from 'utils/ServiceUtils';
import { ReactComponent as IconCommentPlus } from '../../../assets/svg/add-chat.svg';
import { ReactComponent as IconComments } from '../../../assets/svg/comment.svg';
import { ReactComponent as IconEdit } from '../../../assets/svg/ic-edit.svg';
@ -53,7 +55,6 @@ import TagsContainer from '../../Tag/TagsContainer/tags-container';
import TagsViewer from '../../Tag/TagsViewer/tags-viewer';
import EntitySummaryDetails from '../EntitySummaryDetails/EntitySummaryDetails';
import ProfilePicture from '../ProfilePicture/ProfilePicture';
import TitleBreadcrumb from '../title-breadcrumb/title-breadcrumb.component';
import { TitleBreadcrumbProps } from '../title-breadcrumb/title-breadcrumb.interface';
import AnnouncementCard from './AnnouncementCard/AnnouncementCard';
import AnnouncementDrawer from './AnnouncementDrawer/AnnouncementDrawer';
@ -90,6 +91,7 @@ interface Props {
onRestoreEntity?: () => void;
isRecursiveDelete?: boolean;
extraDropdownContent?: ItemType[];
serviceType: string;
}
const EntityPageInfo = ({
@ -122,6 +124,7 @@ const EntityPageInfo = ({
onRestoreEntity,
isRecursiveDelete = false,
extraDropdownContent,
serviceType,
}: Props) => {
const history = useHistory();
const tagThread = entityFieldThreads?.[0];
@ -132,9 +135,6 @@ const EntityPageInfo = ({
const [isViewMore, setIsViewMore] = useState<boolean>(false);
const [tagList, setTagList] = useState<Array<TagOption>>([]);
const [isTagLoading, setIsTagLoading] = useState<boolean>(false);
const [versionFollowButtonWidth, setVersionFollowButtonWidth] = useState(
document.getElementById('version-and-follow-section')?.offsetWidth
);
const [isAnnouncementDrawerOpen, setIsAnnouncementDrawer] =
useState<boolean>(false);
@ -376,101 +376,98 @@ const EntityPageInfo = ({
}, [followersList]);
useAfterMount(() => {
setVersionFollowButtonWidth(
document.getElementById('version-and-follow-section')?.offsetWidth
);
if (ANNOUNCEMENT_ENTITIES.includes(entityType as EntityType)) {
fetchActiveAnnouncement();
}
});
return (
<div data-testid="entity-page-info">
<Space
align="start"
className="tw-justify-between"
style={{ width: '100%' }}>
<Space align="center">
<TitleBreadcrumb
titleLinks={titleLinks}
widthDeductions={
(versionFollowButtonWidth ? versionFollowButtonWidth : 0) + 30
}
/>
{deleted && (
<div className="deleted-badge-button" data-testid="deleted-badge">
<ExclamationCircleOutlined className="tw-mr-1" />
{t('label.deleted')}
</div>
)}
</Space>
<Space align="center" id="version-and-follow-section">
{!isUndefined(version) ? (
<>
{!isUndefined(isVersionSelected) ? (
<Tooltip
placement="bottom"
title={
<p className="tw-text-xs">
{t('message.viewing-older-version')}
</p>
}
trigger="hover">
{getVersionButton(toString(version))}
</Tooltip>
) : (
<>{getVersionButton(toString(version))}</>
)}
</>
) : null}
{!isUndefined(isFollowing) ? (
<Button
className={classNames(
'tw-border tw-border-primary tw-rounded',
isFollowing ? 'tw-text-white' : 'tw-text-primary'
)}
data-testid="follow-button"
size="small"
type={isFollowing ? 'primary' : 'default'}
onClick={() => {
!deleted && followHandler?.();
}}>
<Space>
<StarFilled className="tw-text-xs" />
{isFollowing ? t('label.un-follow') : t('label.follow')}
<Popover content={getFollowers()} trigger="click">
<span
className={classNames(
'tw-border-l tw-font-medium tw-cursor-pointer hover:tw-underline tw-pl-1',
{ 'tw-border-primary': !isFollowing }
)}
data-testid="follower-value"
onClick={(e) => e.stopPropagation()}>
{followers}
</span>
</Popover>
</Space>
</Button>
) : null}
{!isVersionSelected && (
<ManageButton
allowSoftDelete={!deleted}
canDelete={canDelete}
deleted={deleted}
entityFQN={entityFqn}
entityId={entityId}
entityName={entityName}
entityType={entityType}
extraDropdownContent={extraDropdownContent}
isRecursiveDelete={isRecursiveDelete}
onAnnouncementClick={() => setIsAnnouncementDrawer(true)}
onRestoreEntity={onRestoreEntity}
/>
)}
</Space>
</Space>
<Space
className="w-full"
data-testid="entity-page-info"
direction="vertical">
<EntityHeader
breadcrumb={titleLinks}
entityData={{
displayName: entityName,
name: entityName,
deleted,
}}
entityType={(entityType as EntityType) ?? EntityType.TABLE}
extra={
<Space align="center" id="version-and-follow-section">
{!isUndefined(version) ? (
<>
{!isUndefined(isVersionSelected) ? (
<Tooltip
placement="bottom"
title={
<p className="tw-text-xs">
{t('message.viewing-older-version')}
</p>
}
trigger="hover">
{getVersionButton(toString(version))}
</Tooltip>
) : (
<>{getVersionButton(toString(version))}</>
)}
</>
) : null}
{!isUndefined(isFollowing) ? (
<Button
className={classNames(
'tw-border tw-border-primary tw-rounded',
isFollowing ? 'tw-text-white' : 'tw-text-primary'
)}
data-testid="follow-button"
size="small"
type={isFollowing ? 'primary' : 'default'}
onClick={() => {
!deleted && followHandler?.();
}}>
<Space>
<StarFilled className="tw-text-xs" />
{isFollowing ? t('label.un-follow') : t('label.follow')}
<Popover content={getFollowers()} trigger="click">
<span
className={classNames(
'tw-border-l tw-font-medium tw-cursor-pointer hover:tw-underline tw-pl-1',
{ 'tw-border-primary': !isFollowing }
)}
data-testid="follower-value"
onClick={(e) => e.stopPropagation()}>
{followers}
</span>
</Popover>
</Space>
</Button>
) : null}
{!isVersionSelected && (
<ManageButton
allowSoftDelete={!deleted}
canDelete={canDelete}
deleted={deleted}
entityFQN={entityFqn}
entityId={entityId}
entityName={entityName}
entityType={entityType}
extraDropdownContent={extraDropdownContent}
isRecursiveDelete={isRecursiveDelete}
onAnnouncementClick={() => setIsAnnouncementDrawer(true)}
onRestoreEntity={onRestoreEntity}
/>
)}
</Space>
}
icon={
serviceType && (
<img className="h-8" src={serviceTypeLogo(serviceType)} />
)
}
/>
<Space wrap className="tw-justify-between" style={{ width: '100%' }}>
<Space wrap className="justify-between w-full" size={16}>
<Space direction="vertical">
<Space wrap align="center" data-testid="extrainfo" size={4}>
{extraInfo.map((info, index) => (
@ -499,9 +496,7 @@ const EntityPageInfo = ({
{(!isEditable || !isTagEditable || deleted) && (
<>
{(tags.length > 0 || !isEmpty(tier)) && (
<span className="d-flex align-center h-4">
<IconTagGrey height={18} name="icon-tag" width={18} />
</span>
<IconTagGrey height={14} name="icon-tag" />
)}
{tier?.tagFQN && (
<Tags
@ -603,7 +598,7 @@ const EntityPageInfo = ({
onClose={() => setIsAnnouncementDrawer(false)}
/>
)}
</div>
</Space>
);
};

View File

@ -1,110 +0,0 @@
/*
* Copyright 2022 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 { Button, Typography } from 'antd';
import { ReactComponent as IconExternalLink } from 'assets/svg/external-link.svg';
import classNames from 'classnames';
import { toString } from 'lodash';
import React, { useMemo } from 'react';
import { Link } from 'react-router-dom';
import { ROUTES } from '../../../constants/constants';
import { EntityType, FqnPart } from '../../../enums/entity.enum';
import { SearchIndex } from '../../../enums/search.enum';
import {
getNameFromFQN,
getPartialNameFromTableFQN,
} from '../../../utils/CommonUtils';
import { stringToHTML } from '../../../utils/StringsUtils';
import { getEntityLink } from '../../../utils/TableUtils';
import './TableDataCardTitle.less';
interface TableDataCardTitleProps {
dataTestId?: string;
id?: string;
searchIndex: SearchIndex | EntityType;
source: {
fullyQualifiedName?: string;
displayName?: string;
name?: string;
type?: string;
};
isPanel?: boolean;
handleLinkClick?: (e: React.MouseEvent) => void;
openEntityInNewPage?: boolean;
}
const TableDataCardTitle = ({
dataTestId,
id,
searchIndex,
source,
handleLinkClick,
isPanel = false,
openEntityInNewPage = false,
}: TableDataCardTitleProps) => {
const isTourRoute = location.pathname.includes(ROUTES.TOUR);
const { testId, displayName } = useMemo(
() => ({
testId: dataTestId
? dataTestId
: `${getPartialNameFromTableFQN(source.fullyQualifiedName ?? '', [
FqnPart.Service,
])}-${source.name}`,
displayName:
source.type === 'tag'
? toString(getNameFromFQN(source.fullyQualifiedName ?? ''))
: toString(source.displayName),
}),
[dataTestId, source]
);
const title = (
<Button
data-testid={testId}
id={`${id ?? testId}-title`}
type="link"
onClick={isTourRoute ? handleLinkClick : undefined}>
{stringToHTML(displayName)}
{openEntityInNewPage && (
<IconExternalLink className="anticon" height={14} />
)}
</Button>
);
if (isTourRoute) {
return title;
}
return (
<Typography.Title
ellipsis
className="m-b-0 text-base"
level={5}
title={displayName}>
<Link
className={classNames(
'table-data-card-title-container w-fit-content w-max-90',
{
'button-hover': isPanel,
}
)}
target={openEntityInNewPage ? '_blank' : '_self'}
to={getEntityLink(searchIndex, source.fullyQualifiedName ?? '')}>
{title}
</Link>
</Typography.Title>
);
};
export default TableDataCardTitle;

View File

@ -14,7 +14,6 @@
import { render } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { SearchIndex } from '../../../enums/search.enum';
import TableDataCardV2 from './TableDataCardV2';
jest.mock('../../../utils/TableUtils', () => ({
@ -42,13 +41,16 @@ jest.mock('../table-data-card/TableDataCardBody', () => {
const mockHandleSummaryPanelDisplay = jest.fn();
jest.mock('components/Entity/EntityHeader/EntityHeader.component', () => ({
EntityHeader: jest.fn().mockImplementation(() => <p>EntityHeader</p>),
}));
describe('Test TableDataCard Component', () => {
it('Component should render', () => {
const { getByTestId } = render(
const { getByTestId, getByText } = render(
<TableDataCardV2
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
id="1"
searchIndex={SearchIndex.TABLE}
source={{
id: '1',
name: 'Name1',
@ -57,26 +59,9 @@ describe('Test TableDataCard Component', () => {
{ wrapper: MemoryRouter }
);
const tableDataCard = getByTestId('table-data-card');
const entityHeader = getByText('EntityHeader');
expect(tableDataCard).toBeInTheDocument();
});
it('Component should render for deleted', () => {
const { getByTestId } = render(
<TableDataCardV2
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
id="1"
searchIndex={SearchIndex.TABLE}
source={{
id: '2',
name: 'Name2',
deleted: true,
}}
/>,
{ wrapper: MemoryRouter }
);
const deleted = getByTestId('deleted');
expect(deleted).toBeInTheDocument();
expect(entityHeader).toBeInTheDocument();
});
});

View File

@ -11,36 +11,31 @@
* limitations under the License.
*/
import { ExclamationCircleOutlined } from '@ant-design/icons';
import { Checkbox } from 'antd';
import classNames from 'classnames';
import { EntityHeader } from 'components/Entity/EntityHeader/EntityHeader.component';
import { isString, startCase, uniqueId } from 'lodash';
import { ExtraInfo } from 'Models';
import React, { forwardRef, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useParams } from 'react-router-dom';
import { getEntityId, getEntityName } from 'utils/EntityUtils';
import AppState from '../../../AppState';
import { useParams } from 'react-router-dom';
import {
getEntityBreadcrumbs,
getEntityId,
getEntityName,
} from 'utils/EntityUtils';
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import { ROUTES } from '../../../constants/constants';
import { EntityType } from '../../../enums/entity.enum';
import { SearchIndex } from '../../../enums/search.enum';
import { CurrentTourPageType } from '../../../enums/tour.enum';
import { OwnerType } from '../../../enums/user.enum';
import { EntityReference } from '../../../generated/entity/type';
import {
getEntityPlaceHolder,
getOwnerValue,
} from '../../../utils/CommonUtils';
import {
getEntityHeaderLabel,
getServiceIcon,
getUsagePercentile,
} from '../../../utils/TableUtils';
import { getServiceIcon, getUsagePercentile } from '../../../utils/TableUtils';
import { SearchedDataProps } from '../../searched-data/SearchedData.interface';
import '../table-data-card/TableDataCard.style.css';
import TableDataCardBody from '../table-data-card/TableDataCardBody';
import TableDataCardTitle from './TableDataCardTitle.component';
import './TableDataCardV2.less';
export interface TableDataCardPropsV2 {
@ -51,7 +46,6 @@ export interface TableDataCardPropsV2 {
key: string;
value: number;
}[];
searchIndex: SearchIndex | EntityType;
handleSummaryPanelDisplay?: (
details: SearchedDataProps['data'][number]['_source'],
entityType: string
@ -71,16 +65,14 @@ const TableDataCardV2: React.FC<TableDataCardPropsV2> = forwardRef<
className,
source,
matches,
searchIndex,
handleSummaryPanelDisplay,
showCheckboxes,
checked,
openEntityInNewPage = false,
openEntityInNewPage,
},
ref
) => {
const { t } = useTranslation();
const location = useLocation();
const { tab } = useParams<{ tab: string }>();
const otherDetails = useMemo(() => {
@ -137,21 +129,15 @@ const TableDataCardV2: React.FC<TableDataCardPropsV2> = forwardRef<
return _otherDetails;
}, [source]);
const handleLinkClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (location.pathname.includes(ROUTES.TOUR)) {
AppState.currentTourPage = CurrentTourPageType.DATASET_PAGE;
}
};
const headerLabel = useMemo(() => {
return getEntityHeaderLabel(source);
}, [source]);
const serviceIcon = useMemo(() => {
return getServiceIcon(source);
}, [source]);
const breadcrumbs = useMemo(
() => getEntityBreadcrumbs(source, source.entityType as EntityType),
[source]
);
return (
<div
className={classNames(
@ -165,33 +151,20 @@ const TableDataCardV2: React.FC<TableDataCardPropsV2> = forwardRef<
onClick={() => {
handleSummaryPanelDisplay && handleSummaryPanelDisplay(source, tab);
}}>
<div>
{showCheckboxes && (
<Checkbox checked={checked} className="float-right" />
)}
{headerLabel}
<div className="tw-flex tw-items-center">
{serviceIcon}
<TableDataCardTitle
handleLinkClick={handleLinkClick}
id={id}
openEntityInNewPage={openEntityInNewPage}
searchIndex={searchIndex}
source={source}
/>
<EntityHeader
titleIsLink
breadcrumb={breadcrumbs}
entityData={source}
entityType={source.entityType as EntityType}
extra={
showCheckboxes && (
<Checkbox checked={checked} className="m-l-auto" />
)
}
icon={serviceIcon}
openEntityInNewPage={openEntityInNewPage}
/>
{source.deleted && (
<>
<div
className="tw-rounded tw-bg-error-lite tw-text-error tw-text-xs tw-font-medium tw-h-5 tw-px-1.5 tw-py-0.5 tw-ml-2"
data-testid="deleted">
<ExclamationCircleOutlined className="tw-mr-1" />
{t('label.deleted')}
</div>
</>
)}
</div>
</div>
<div className="tw-pt-3">
<TableDataCardBody
description={source.description || ''}

View File

@ -13,9 +13,10 @@
import classNames from 'classnames';
import { ELASTICSEARCH_ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
import { isUndefined, toString } from 'lodash';
import { isUndefined } from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import { getEntityName } from 'utils/EntityUtils';
import { PAGE_SIZE } from '../../constants/constants';
import { MAX_RESULT_HITS } from '../../constants/explore.constants';
import { Paging } from '../../generated/type/paging';
@ -51,7 +52,7 @@ const SearchedData: React.FC<SearchedDataProps> = ({
handleSummaryPanelDisplay,
}) => {
const highlightSearchResult = () => {
return data.map(({ _source: table, highlight, _index }, index) => {
return data.map(({ _source: table, highlight }, index) => {
let tDesc = table.description ?? '';
const highLightedTexts = highlight?.description || [];
@ -65,7 +66,7 @@ const SearchedData: React.FC<SearchedDataProps> = ({
});
}
let name = toString(table.displayName);
let name = getEntityName(table);
if (!isUndefined(highlight)) {
name = highlight?.name?.join(' ') || name;
}
@ -102,7 +103,6 @@ const SearchedData: React.FC<SearchedDataProps> = ({
handleSummaryPanelDisplay={handleSummaryPanelDisplay}
id={`tabledatacard${index}`}
matches={matches}
searchIndex={_index}
source={{ ...table, name, description: tDesc }}
/>
</div>

View File

@ -282,6 +282,7 @@
"enter-property-value": "Enter Property Value",
"enter-type-password": "Enter {{type}} Password",
"entity-count": "{{entity}} Count",
"entity-detail-plural": "{{entity}} details",
"entity-hyphen-value": "{{entity}} - {{value}}",
"entity-index": "{{entity}} index",
"entity-name": "{{entity}} Name",

View File

@ -282,6 +282,7 @@
"enter-property-value": "Ingrese el valor de la propiedad",
"enter-type-password": "Ingrese la contraseña de {{type}}",
"entity-count": "Cantidad de {{entity}}",
"entity-detail-plural": "{{entity}} details",
"entity-hyphen-value": "{{entity}} - {{value}}",
"entity-index": "Índice de {{entity}}",
"entity-name": "Nombre de {{entity}}",

View File

@ -282,6 +282,7 @@
"enter-property-value": "Entrer une Valeur pour la Propriété",
"enter-type-password": "Enter {{type}} Password",
"entity-count": "{{entity}} Count",
"entity-detail-plural": "{{entity}} details",
"entity-hyphen-value": "{{entity}} - {{value}}",
"entity-index": "{{entity}} index",
"entity-name": "{{entity}} Name",

View File

@ -282,6 +282,7 @@
"enter-property-value": "プロパティの値を入力",
"enter-type-password": "{{type}} のパスワードを入力",
"entity-count": "{{entity}}の数",
"entity-detail-plural": "{{entity}} details",
"entity-hyphen-value": "{{entity}} - {{value}}",
"entity-index": "{{entity}} インデックス",
"entity-name": "{{entity}} 名",

View File

@ -282,6 +282,7 @@
"enter-property-value": "Introduzir valor da propriedade",
"enter-type-password": "Introduzir {{type}} da senha",
"entity-count": "{{entity}} contagem",
"entity-detail-plural": "{{entity}} details",
"entity-hyphen-value": "{{entity}} - {{value}}",
"entity-index": "Indíce da {{entity}}",
"entity-name": "Nome da {{entity}}",

View File

@ -282,6 +282,7 @@
"enter-property-value": "输入属性值",
"enter-type-password": "输入 {{type}} 密码",
"entity-count": "{{entity}} Count",
"entity-detail-plural": "{{entity}} details",
"entity-hyphen-value": "{{entity}} - {{value}}",
"entity-index": "{{entity}} index",
"entity-name": "{{entity}} Name",

View File

@ -72,7 +72,6 @@ import { getContainerDetailPath } from 'utils/ContainerDetailUtils';
import { getEntityLineage, getEntityName } from 'utils/EntityUtils';
import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils';
import { getLineageViewPath } from 'utils/RouterUtils';
import { serviceTypeLogo } from 'utils/ServiceUtils';
import { bytesToSize } from 'utils/StringsUtils';
import { getTagsWithoutTier, getTierTags } from 'utils/TableUtils';
import { showErrorToast, showSuccessToast } from 'utils/ToastUtils';
@ -286,7 +285,6 @@ const ContainerPage = () => {
];
const breadcrumbTitles = useMemo(() => {
const serviceType = containerData?.serviceType;
const service = containerData?.service;
const serviceName = service?.name;
@ -301,14 +299,8 @@ const ContainerPage = () => {
url: serviceName
? getServiceDetailsPath(serviceName, ServiceCategory.STORAGE_SERVICES)
: '',
imgSrc: serviceType ? serviceTypeLogo(serviceType) : undefined,
},
...parentContainerItems,
{
name: entityName,
url: '',
activeTitle: true,
},
];
}, [containerData, containerName, entityName, parentContainers]);
@ -632,6 +624,7 @@ const ContainerPage = () => {
isFollowing={isUserFollowing}
isTagEditable={hasEditTagsPermission}
removeTier={hasEditTierPermission ? handleRemoveTier : undefined}
serviceType={containerData?.serviceType ?? ''}
tags={tags}
tagsHandler={handleUpdateTags}
tier={tier}

View File

@ -65,7 +65,6 @@ import {
import { getEntityFeedLink, getEntityName } from '../../utils/EntityUtils';
import { deletePost, updateThreadData } from '../../utils/FeedUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import { serviceTypeLogo } from '../../utils/ServiceUtils';
import { showErrorToast } from '../../utils/ToastUtils';
export type ChartType = {
@ -242,12 +241,6 @@ const DashboardDetailsPage = () => {
ServiceCategory.DASHBOARD_SERVICES
)
: '',
imgSrc: serviceType ? serviceTypeLogo(serviceType) : undefined,
},
{
name: getEntityName(res),
url: '',
activeTitle: true,
},
]);

View File

@ -33,6 +33,7 @@ import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichText
import TabsPane from 'components/common/TabsPane/TabsPane';
import { TitleBreadcrumbProps } from 'components/common/title-breadcrumb/title-breadcrumb.interface';
import PageContainerV1 from 'components/containers/PageContainerV1';
import PageLayoutV1 from 'components/containers/PageLayoutV1';
import Loader from 'components/Loader/Loader';
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
import {
@ -110,10 +111,7 @@ import {
} from '../../utils/FeedUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import { getSettingPath } from '../../utils/RouterUtils';
import {
getServiceRouteFromServiceType,
serviceTypeLogo,
} from '../../utils/ServiceUtils';
import { getServiceRouteFromServiceType } from '../../utils/ServiceUtils';
import { getErrorText } from '../../utils/StringsUtils';
import {
getEntityLink,
@ -283,7 +281,6 @@ const DatabaseSchemaPage: FunctionComponent = () => {
id = '',
name,
service,
serviceType,
database,
tags,
} = res;
@ -312,7 +309,6 @@ const DatabaseSchemaPage: FunctionComponent = () => {
ServiceCategory.DATABASE_SERVICES
)
: '',
imgSrc: serviceType ? serviceTypeLogo(serviceType) : undefined,
},
{
name: getPartialNameFromTableFQN(
@ -321,11 +317,6 @@ const DatabaseSchemaPage: FunctionComponent = () => {
),
url: getDatabaseDetailsPath(database.fullyQualifiedName ?? ''),
},
{
name: getEntityName(res),
url: '',
activeTitle: true,
},
]);
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
@ -634,7 +625,7 @@ const DatabaseSchemaPage: FunctionComponent = () => {
const getSchemaTableList = () => {
return (
<>
<Col span={24}>
<TableAntd
bordered
className="table-shadow"
@ -659,7 +650,7 @@ const DatabaseSchemaPage: FunctionComponent = () => {
totalCount={tableInstanceCount}
/>
)}
</>
</Col>
);
};
@ -785,10 +776,10 @@ const DatabaseSchemaPage: FunctionComponent = () => {
{databaseSchemaPermission.ViewAll ||
databaseSchemaPermission.ViewBasic ? (
<PageContainerV1>
<Row
className="p-x-md p-t-lg"
data-testid="page-container"
gutter={[0, 12]}>
<PageLayoutV1
pageTitle={t('label.entity-detail-plural', {
entity: getEntityName(databaseSchema),
})}>
{IsSchemaDetailsLoading ? (
<Skeleton
active
@ -820,6 +811,7 @@ const DatabaseSchemaPage: FunctionComponent = () => {
databaseSchemaPermission.EditAll ||
databaseSchemaPermission.EditTags
}
serviceType={databaseSchema?.serviceType ?? ''}
tags={tags}
tagsHandler={onTagUpdate}
tier={tier}
@ -834,27 +826,6 @@ const DatabaseSchemaPage: FunctionComponent = () => {
onThreadLinkSelect={onThreadLinkSelect}
/>
</Col>
<Col data-testid="description-container" span={24}>
<Description
description={description}
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.DESCRIPTION,
entityFieldThreadCount
)}
entityFqn={databaseSchemaFQN}
entityName={databaseSchemaName}
entityType={EntityType.DATABASE_SCHEMA}
hasEditAccess={
databaseSchemaPermission.EditDescription ||
databaseSchemaPermission.EditAll
}
isEdit={isEdit}
onCancel={onCancel}
onDescriptionEdit={onDescriptionEdit}
onDescriptionUpdate={onDescriptionUpdate}
onThreadLinkSelect={onThreadLinkSelect}
/>
</Col>
</>
)}
<Col span={24}>
@ -869,7 +840,32 @@ const DatabaseSchemaPage: FunctionComponent = () => {
</Col>
<Col className="p-y-md" span={24}>
{activeTab === 1 && (
<Fragment>{getSchemaTableList()}</Fragment>
<Card className="h-full">
<Row gutter={[16, 16]}>
<Col data-testid="description-container" span={24}>
<Description
description={description}
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.DESCRIPTION,
entityFieldThreadCount
)}
entityFqn={databaseSchemaFQN}
entityName={databaseSchemaName}
entityType={EntityType.DATABASE_SCHEMA}
hasEditAccess={
databaseSchemaPermission.EditDescription ||
databaseSchemaPermission.EditAll
}
isEdit={isEdit}
onCancel={onCancel}
onDescriptionEdit={onDescriptionEdit}
onDescriptionUpdate={onDescriptionUpdate}
onThreadLinkSelect={onThreadLinkSelect}
/>
</Col>
{getSchemaTableList()}
</Row>
</Card>
)}
{activeTab === 2 && (
<Card className="p-t-xss p-b-md">
@ -914,7 +910,7 @@ const DatabaseSchemaPage: FunctionComponent = () => {
/>
) : null}
</Col>
</Row>
</PageLayoutV1>
</PageContainerV1>
) : (
<ErrorPlaceHolder>

View File

@ -181,6 +181,10 @@ jest.mock('react-router-dom', () => ({
})),
}));
jest.mock('components/containers/PageLayoutV1', () => {
return jest.fn().mockImplementation(({ children }) => children);
});
describe('Tests for DatabaseSchemaPage', () => {
it('Page should render properly for "Tables" tab', async () => {
act(() => {

View File

@ -76,7 +76,6 @@ import {
import { getEntityFeedLink, getEntityName } from '../../utils/EntityUtils';
import { deletePost, updateThreadData } from '../../utils/FeedUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import { serviceTypeLogo } from '../../utils/ServiceUtils';
import { showErrorToast } from '../../utils/ToastUtils';
const DatasetDetailsPage: FunctionComponent = () => {
@ -234,7 +233,6 @@ const DatasetDetailsPage: FunctionComponent = () => {
ServiceCategory.DATABASE_SERVICES
)
: '',
imgSrc: serviceType ? serviceTypeLogo(serviceType) : undefined,
},
{
name: getPartialNameFromTableFQN(databaseFullyQualifiedName, [
@ -251,11 +249,6 @@ const DatasetDetailsPage: FunctionComponent = () => {
databaseSchemaFullyQualifiedName
),
},
{
name: getEntityName(res),
url: '',
activeTitle: true,
},
]);
addToRecentViewed({

View File

@ -206,7 +206,6 @@ const EntityVersionPage: FunctionComponent = () => {
tags = [],
database,
service,
serviceType,
databaseSchema,
} = res;
const serviceName = service?.name ?? '';
@ -219,7 +218,6 @@ const EntityVersionPage: FunctionComponent = () => {
ServiceCategory.DATABASE_SERVICES
)
: '',
imgSrc: serviceType ? serviceTypeLogo(serviceType) : undefined,
},
{
name: getPartialNameFromTableFQN(
@ -237,11 +235,6 @@ const EntityVersionPage: FunctionComponent = () => {
databaseSchema?.fullyQualifiedName ?? ''
),
},
{
name: getEntityName(res),
url: '',
activeTitle: true,
},
]);
getTableVersions(id)

View File

@ -51,7 +51,6 @@ import {
defaultFields,
getFormattedPipelineDetails,
} from '../../utils/PipelineDetailsUtils';
import { serviceTypeLogo } from '../../utils/ServiceUtils';
import { showErrorToast } from '../../utils/ToastUtils';
const PipelineDetailsPage = () => {
@ -134,12 +133,6 @@ const PipelineDetailsPage = () => {
ServiceCategory.PIPELINE_SERVICES
)
: '',
imgSrc: serviceType ? serviceTypeLogo(serviceType) : undefined,
},
{
name: getEntityName(res),
url: '',
activeTitle: true,
},
]);

View File

@ -58,7 +58,6 @@ import {
import { getEntityFeedLink, getEntityName } from '../../utils/EntityUtils';
import { deletePost, updateThreadData } from '../../utils/FeedUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import { serviceTypeLogo } from '../../utils/ServiceUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import {
getCurrentTopicTab,
@ -210,12 +209,6 @@ const TopicDetailsPage: FunctionComponent = () => {
ServiceCategory.MESSAGING_SERVICES
)
: '',
imgSrc: serviceType ? serviceTypeLogo(serviceType) : undefined,
},
{
name: getEntityName(res),
url: '',
activeTitle: true,
},
]);

View File

@ -333,12 +333,13 @@ jest.mock('components/common/DeleteWidget/DeleteWidgetModal', () => {
<p data-testid="delete-entity">DeleteWidgetModal component</p>
);
});
const mockObserve = jest.fn();
const mockunObserve = jest.fn();
window.IntersectionObserver = jest.fn().mockImplementation(() => ({
observe: mockObserve,
unobserve: mockunObserve,
jest.mock('components/containers/PageLayoutV1', () => {
return jest.fn().mockImplementation(({ children }) => children);
});
jest.mock('components/Entity/EntityHeader/EntityHeader.component', () => ({
EntityHeader: jest.fn().mockImplementation(() => <p>EntityHeader</p>),
}));
describe('Test DatabaseDetails page', () => {
@ -347,8 +348,7 @@ describe('Test DatabaseDetails page', () => {
wrapper: MemoryRouter,
});
const pageContainer = await findByTestId(container, 'page-container');
const titleBreadcrumb = await findByText(container, /TitleBreadcrumb/i);
const entityHeader = await findByText(container, 'EntityHeader');
const descriptionContainer = await findByTestId(
container,
'description-container'
@ -358,8 +358,7 @@ describe('Test DatabaseDetails page', () => {
'database-databaseSchemas'
);
expect(pageContainer).toBeInTheDocument();
expect(titleBreadcrumb).toBeInTheDocument();
expect(entityHeader).toBeInTheDocument();
expect(descriptionContainer).toBeInTheDocument();
expect(databaseTable).toBeInTheDocument();
});
@ -420,8 +419,7 @@ describe('Test DatabaseDetails page', () => {
wrapper: MemoryRouter,
});
const pageContainer = await findByTestId(container, 'page-container');
const titleBreadcrumb = await findByText(container, /TitleBreadcrumb/i);
const entityHeader = await findByText(container, 'EntityHeader');
const descriptionContainer = await findByTestId(
container,
'description-container'
@ -431,8 +429,7 @@ describe('Test DatabaseDetails page', () => {
'database-databaseSchemas'
);
expect(pageContainer).toBeInTheDocument();
expect(titleBreadcrumb).toBeInTheDocument();
expect(entityHeader).toBeInTheDocument();
expect(descriptionContainer).toBeInTheDocument();
expect(databaseTable).toBeInTheDocument();
});

View File

@ -23,9 +23,10 @@ import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlac
import NextPrevious from 'components/common/next-previous/NextPrevious';
import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer';
import TabsPane from 'components/common/TabsPane/TabsPane';
import TitleBreadcrumb from 'components/common/title-breadcrumb/title-breadcrumb.component';
import { TitleBreadcrumbProps } from 'components/common/title-breadcrumb/title-breadcrumb.interface';
import PageContainerV1 from 'components/containers/PageContainerV1';
import PageLayoutV1 from 'components/containers/PageLayoutV1';
import { EntityHeader } from 'components/Entity/EntityHeader/EntityHeader.component';
import Loader from 'components/Loader/Loader';
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
import {
@ -314,12 +315,6 @@ const DatabaseDetails: FunctionComponent = () => {
ServiceCategory.DATABASE_SERVICES
)
: '',
imgSrc: serviceType ? serviceTypeLogo(serviceType) : undefined,
},
{
name: getEntityName(res),
url: '',
activeTitle: true,
},
]);
fetchDatabaseSchemasAndDBTModels();
@ -658,10 +653,10 @@ const DatabaseDetails: FunctionComponent = () => {
<>
{databasePermission.ViewAll || databasePermission.ViewBasic ? (
<PageContainerV1>
<Row
className=" p-x-md p-t-lg"
data-testid="page-container"
gutter={[0, 12]}>
<PageLayoutV1
pageTitle={t('label.entity-detail-plural', {
entity: getEntityName(database),
})}>
{isDatabaseDetailsLoading ? (
<Skeleton
active
@ -672,20 +667,31 @@ const DatabaseDetails: FunctionComponent = () => {
/>
) : (
<>
<Col span={24}>
<Space align="center" className="justify-between w-full">
<TitleBreadcrumb titleLinks={slashedDatabaseName} />
<ManageButton
isRecursiveDelete
allowSoftDelete={false}
canDelete={databasePermission.Delete}
entityFQN={databaseFQN}
entityId={databaseId}
entityName={databaseName}
entityType={EntityType.DATABASE}
/>
</Space>
</Col>
{database && (
<EntityHeader
breadcrumb={slashedDatabaseName}
entityData={database}
entityType={EntityType.DATABASE}
extra={
<ManageButton
isRecursiveDelete
allowSoftDelete={false}
canDelete={databasePermission.Delete}
entityFQN={databaseFQN}
entityId={databaseId}
entityName={databaseName}
entityType={EntityType.DATABASE}
/>
}
icon={
<img
className="h-8"
src={serviceTypeLogo(serviceType ?? '')}
/>
}
/>
)}
<Col span={24}>
{extraInfo.map((info, index) => (
<Space key={index}>
@ -809,7 +815,7 @@ const DatabaseDetails: FunctionComponent = () => {
/>
) : null}
</Col>
</Row>
</PageLayoutV1>
</PageContainerV1>
) : (
<ErrorPlaceHolder>

View File

@ -271,6 +271,10 @@ jest.mock('../../utils/ToastUtils', () => ({
showErrorToast: jest.fn(),
}));
jest.mock('components/containers/PageLayoutV1', () => {
return jest.fn().mockImplementation(({ children }) => children);
});
describe('Test ServicePage Component', () => {
it('Component should render', async () => {
const { container } = render(<ServicePage />, {

View File

@ -24,10 +24,11 @@ import ProfilePicture from 'components/common/ProfilePicture/ProfilePicture';
import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer';
import TabsPane from 'components/common/TabsPane/TabsPane';
import TestConnection from 'components/common/TestConnection/TestConnection';
import TitleBreadcrumb from 'components/common/title-breadcrumb/title-breadcrumb.component';
import { TitleBreadcrumbProps } from 'components/common/title-breadcrumb/title-breadcrumb.interface';
import PageContainerV1 from 'components/containers/PageContainerV1';
import PageLayoutV1 from 'components/containers/PageLayoutV1';
import DataModelTable from 'components/DataModels/DataModelsTable';
import { EntityHeader } from 'components/Entity/EntityHeader/EntityHeader.component';
import Ingestion from 'components/Ingestion/Ingestion.component';
import Loader from 'components/Loader/Loader';
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
@ -702,7 +703,7 @@ const ServicePage: FunctionComponent = () => {
getServiceByFQN(serviceName, serviceFQN, 'owner')
.then((resService) => {
if (resService) {
const { description, serviceType } = resService;
const { description } = resService;
setServiceDetails(resService);
setConnectionDetails(
resService.connection?.config as DashboardConnection
@ -716,12 +717,6 @@ const ServicePage: FunctionComponent = () => {
getServiceRouteFromServiceType(serviceName)
),
},
{
name: getEntityName(resService),
url: '',
imgSrc: serviceType ? serviceTypeLogo(serviceType) : undefined,
activeTitle: true,
},
]);
getOtherDetails();
} else {
@ -788,6 +783,30 @@ const ServicePage: FunctionComponent = () => {
}
};
const handleRemoveOwner = async () => {
const updatedData = {
...serviceDetails,
owner: undefined,
} as ServicesUpdateRequest;
const jsonPatch = compare(serviceDetails || {}, updatedData);
try {
const res = await updateOwnerService(
serviceName,
serviceDetails?.id ?? '',
jsonPatch
);
setServiceDetails(res);
} catch (error) {
showErrorToast(
error as AxiosError,
t('server.entity-updating-error', {
entity: t('label.owner-lowercase'),
})
);
}
};
const handleUpdateOwner = (owner: ServicesType['owner']) => {
if (isUndefined(owner)) {
handleRemoveOwner();
@ -832,30 +851,6 @@ const ServicePage: FunctionComponent = () => {
});
};
const handleRemoveOwner = async () => {
const updatedData = {
...serviceDetails,
owner: undefined,
} as ServicesUpdateRequest;
const jsonPatch = compare(serviceDetails || {}, updatedData);
try {
const res = await updateOwnerService(
serviceName,
serviceDetails?.id ?? '',
jsonPatch
);
setServiceDetails(res);
} catch (error) {
showErrorToast(
error as AxiosError,
t('server.entity-updating-error', {
entity: t('label.owner-lowercase'),
})
);
}
};
const onDescriptionEdit = (): void => {
setIsEdit(true);
};
@ -1018,69 +1013,53 @@ const ServicePage: FunctionComponent = () => {
{getEntityMissingError(serviceName as string, serviceFQN)}
</ErrorPlaceHolder>
) : (
<>
<PageLayoutV1
pageTitle={t('label.entity-detail-plural', {
entity: getEntityName(serviceDetails),
})}>
{servicePermission.ViewAll || servicePermission.ViewBasic ? (
<Row
className="p-x-md p-t-lg"
data-testid="service-page"
gutter={[0, 12]}>
<Col span={24}>
<Space align="center" className="justify-between w-full">
<TitleBreadcrumb titleLinks={slashedTableName} />
{serviceDetails?.serviceType !==
MetadataServiceType.OpenMetadata && (
<Tooltip
placement="topRight"
title={
!servicePermission.Delete &&
t('message.no-permission-for-action')
}>
<Button
ghost
data-testid="service-delete"
disabled={!servicePermission.Delete}
icon={
<IcDeleteColored
className="anticon"
height={14}
viewBox="0 0 24 24"
width={14}
/>
}
size="small"
type="primary"
onClick={handleDelete}>
{t('label.delete')}
</Button>
</Tooltip>
)}
<DeleteWidgetModal
isRecursiveDelete
afterDeleteAction={() =>
history.push(
getSettingPath(
GlobalSettingsMenuCategory.SERVICES,
SERVICE_CATEGORY_TYPE[
serviceCategory as keyof typeof SERVICE_CATEGORY_TYPE
]
)
)
}
allowSoftDelete={false}
deleteMessage={getDeleteEntityMessage(
serviceName || '',
paging.total,
schemaCount,
tableCount
)}
entityId={serviceDetails?.id}
entityName={serviceDetails?.name || ''}
entityType={serviceName?.slice(0, -1)}
visible={deleteWidgetVisible}
onCancel={() => setDeleteWidgetVisible(false)}
/>
</Space>
</Col>
<Row data-testid="service-page" gutter={[0, 12]}>
{serviceDetails && (
<EntityHeader
breadcrumb={slashedTableName}
entityData={serviceDetails}
extra={
serviceDetails?.serviceType !==
MetadataServiceType.OpenMetadata && (
<Tooltip
placement="topRight"
title={
!servicePermission.Delete &&
t('message.no-permission-for-action')
}>
<Button
ghost
data-testid="service-delete"
disabled={!servicePermission.Delete}
icon={
<IcDeleteColored
className="anticon"
height={14}
viewBox="0 0 24 24"
width={14}
/>
}
size="small"
type="primary"
onClick={handleDelete}>
{t('label.delete')}
</Button>
</Tooltip>
)
}
icon={
<img
className="h-8"
src={serviceTypeLogo(serviceDetails.serviceType)}
/>
}
/>
)}
<Col span={24}>
<Space>
{extraInfo.map((info) => (
@ -1222,7 +1201,33 @@ const ServicePage: FunctionComponent = () => {
{t('message.no-permission-to-view')}
</ErrorPlaceHolder>
)}
</>
<DeleteWidgetModal
isRecursiveDelete
afterDeleteAction={() =>
history.push(
getSettingPath(
GlobalSettingsMenuCategory.SERVICES,
SERVICE_CATEGORY_TYPE[
serviceCategory as keyof typeof SERVICE_CATEGORY_TYPE
]
)
)
}
allowSoftDelete={false}
deleteMessage={getDeleteEntityMessage(
serviceName || '',
paging.total,
schemaCount,
tableCount
)}
entityId={serviceDetails?.id}
entityName={serviceDetails?.name || ''}
entityType={serviceName?.slice(0, -1)}
visible={deleteWidgetVisible}
onCancel={() => setDeleteWidgetVisible(false)}
/>
</PageLayoutV1>
)}
</PageContainerV1>
);

View File

@ -529,8 +529,9 @@ const TagsPage = () => {
useEffect(() => {
/**
* Fetch all classifications initially
* Do not set current if we already have currentClassification set
*/
fetchClassifications(true);
fetchClassifications(!tagCategoryName);
}, []);
useEffect(() => {

View File

@ -47,17 +47,6 @@
box-shadow: @panels-shadow-color;
}
//font weight
.font-300 {
font-weight: 300;
}
.font-medium {
font-weight: 500;
}
.text-600 {
font-weight: 600;
}
// text color
.text-primary {
color: @primary;
@ -244,17 +233,6 @@
}
}
// Font Weight
.font-normal {
font-weight: 500;
}
.font-semibold {
font-weight: 600;
}
.font-bold {
font-weight: 700;
}
.transform-180 {
transform: rotate(180deg);
}
@ -293,6 +271,8 @@
font-weight: 700;
font-size: 18px;
line-height: 22px;
color: @text-color !important;
text-decoration: none !important;
}
/* Opacity CSS start */

View File

@ -34,18 +34,3 @@ button {
color: initial;
opacity: 0.45;
}
// CheckBox
.ant-checkbox-inner {
border-width: 2px;
height: 18px;
width: 18px;
border-color: #7147e8;
}
.ant-checkbox-inner::after {
top: 48%;
left: 18.5%;
width: 6.25px;
height: 10px;
}

View File

@ -140,6 +140,9 @@
}
//Height
.h-inherit {
height: inherit;
}
.h-3 {
height: 12px;
}

View File

@ -12,6 +12,9 @@
*/
//font weight
.font-thin {
font-weight: 300;
}
.font-normal {
font-weight: 400;
}

View File

@ -164,3 +164,8 @@
.float-right {
float: right;
}
// Vertical Align
.vertical-baseline {
vertical-align: baseline;
}

View File

@ -18,12 +18,19 @@ import {
LeafNodes,
LineagePos,
} from 'components/EntityLineage/EntityLineage.interface';
import { EntityUnion } from 'components/Explore/explore.interface';
import {
EntityUnion,
EntityWithServices,
} from 'components/Explore/explore.interface';
import { ResourceEntity } from 'components/PermissionProvider/PermissionProvider.interface';
import { SearchedDataProps } from 'components/searched-data/SearchedData.interface';
import { ExplorePageTabs } from 'enums/Explore.enum';
import { Tag } from 'generated/entity/classification/tag';
import { Container } from 'generated/entity/data/container';
import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel';
import { GlossaryTerm } from 'generated/entity/data/glossaryTerm';
import { Mlmodel } from 'generated/entity/data/mlmodel';
import { Topic } from 'generated/entity/data/topic';
import i18next from 'i18next';
import { get, isEmpty, isNil, isUndefined, lowerCase, startCase } from 'lodash';
import { Bucket, EntityDetailUnion } from 'Models';
@ -34,8 +41,13 @@ import {
getDashboardDetailsPath,
getDatabaseDetailsPath,
getDatabaseSchemaDetailsPath,
getGlossaryTermDetailsPath,
getMlModelDetailsPath,
getPipelineDetailsPath,
getServiceDetailsPath,
getTableDetailsPath,
getTagsDetailsPath,
getTopicDetailsPath,
} from '../constants/constants';
import { AssetsType, EntityType, FqnPart } from '../enums/entity.enum';
import { SearchIndex } from '../enums/search.enum';
@ -58,6 +70,9 @@ import {
getPartialNameFromTableFQN,
getTableFQNFromColumnFQN,
} from './CommonUtils';
import { getContainerDetailPath } from './ContainerDetailUtils';
import Fqn from './Fqn';
import { getGlossaryPath } from './RouterUtils';
import {
getDataTypeString,
getTierFromTableTags,
@ -884,3 +899,151 @@ export const getEntityReferenceListFromEntities = <
return entities.map((entity) => getEntityReferenceFromEntity(entity, type));
};
export const getBreadcrumbForTable = (
entity: Table,
includeCurrent = false
) => {
const { service, database, databaseSchema } = entity;
return [
{
name: getEntityName(service),
url: service?.name
? getServiceDetailsPath(
service?.name,
ServiceCategory.DATABASE_SERVICES
)
: '',
},
{
name: getEntityName(database),
url: getDatabaseDetailsPath(database?.fullyQualifiedName ?? ''),
},
{
name: getEntityName(databaseSchema),
url: getDatabaseSchemaDetailsPath(
databaseSchema?.fullyQualifiedName ?? ''
),
},
...(includeCurrent
? [
{
name: getEntityName(entity),
url: '#',
},
]
: []),
];
};
export const getBreadcrumbForEntitiesWithServiceOnly = (
entity: EntityWithServices,
includeCurrent = false
) => {
const { service } = entity;
const serviceType =
service?.type === 'objectStoreService'
? ServiceCategory.STORAGE_SERVICES
: service?.type;
return [
{
name: getEntityName(service),
url: service?.name
? getServiceDetailsPath(service?.name, serviceType)
: '',
},
...(includeCurrent
? [
{
name: getEntityName(entity),
url: '#',
},
]
: []),
];
};
export const getEntityBreadcrumbs = (
entity: SearchedDataProps['data'][number]['_source'],
entityType?: EntityType,
includeCurrent = false
) => {
switch (entityType) {
case EntityType.TABLE:
return getBreadcrumbForTable(entity as Table, includeCurrent);
case EntityType.GLOSSARY:
case EntityType.GLOSSARY_TERM:
// eslint-disable-next-line no-case-declarations
const glossary = (entity as GlossaryTerm).glossary;
// eslint-disable-next-line no-case-declarations
const fqnList = Fqn.split((entity as GlossaryTerm).fullyQualifiedName);
// eslint-disable-next-line no-case-declarations
const tree = fqnList.slice(1, fqnList.length - 1);
return [
{
name: glossary.fullyQualifiedName,
url: getGlossaryPath(glossary.fullyQualifiedName),
},
...tree.map((fqn, index, source) => ({
name: fqn,
url: getGlossaryPath(
`${glossary.fullyQualifiedName}.${source
.slice(0, index + 1)
.join('.')}`
),
})),
];
case EntityType.TAG:
return [
{
name: getEntityName((entity as Tag).classification),
url: getTagsDetailsPath(
(entity as Tag).classification?.fullyQualifiedName ?? ''
),
},
];
case EntityType.TOPIC:
case EntityType.DASHBOARD:
case EntityType.PIPELINE:
case EntityType.MLMODEL:
case EntityType.CONTAINER:
default:
return getBreadcrumbForEntitiesWithServiceOnly(
entity as Topic,
includeCurrent
);
}
};
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 '';
}
};

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import { ReactComponent as ContainerIcon } from 'assets/svg/ic-storage.svg';
import { AxiosError } from 'axios';
import {
OperationPermission,
@ -301,6 +302,8 @@ export const serviceTypeLogo = (type: string) => {
logo = DATABASE_DEFAULT;
} else if (serviceTypes.mlmodelServices.includes(type)) {
logo = ML_MODEL_DEFAULT;
} else if (serviceTypes.storageServices.includes(type)) {
logo = ContainerIcon;
} else {
logo = DEFAULT_SERVICE;
}

View File

@ -14,7 +14,9 @@
import Icon from '@ant-design/icons';
import { Tooltip } from 'antd';
import { ExpandableConfig } from 'antd/lib/table/interface';
import { ReactComponent as IconFlatFolder } from 'assets/svg/folder.svg';
import { ReactComponent as ContainerIcon } from 'assets/svg/ic-storage.svg';
import { ReactComponent as IconTag } from 'assets/svg/tag-grey.svg';
import classNames from 'classnames';
import { SourceType } from 'components/searched-data/SearchedData.interface';
import { t } from 'i18next';
@ -272,15 +274,15 @@ export const getEntityLink = (
export const getServiceIcon = (source: SourceType) => {
if (source.entityType === EntityType.GLOSSARY_TERM) {
return <SVGIcons alt="icon" className="m-r-xs" icon={Icons.FLAT_FOLDER} />;
return <IconFlatFolder className="h-9" />;
} else if (source.entityType === EntityType.TAG) {
return <SVGIcons alt="icon" className="m-r-xs" icon={Icons.TAG} />;
return <IconTag className="h-9" />;
} else {
return (
<img
alt="service-icon"
className="inline h-8 p-r-xs"
src={serviceTypeLogo((source.serviceType || source.entityType) ?? '')}
className="inline h-9"
src={serviceTypeLogo(source.serviceType || '')}
/>
);
}