fix(ui): right panel for lineage mode (#9999)

* fix(ui): right panel for lineage mode

* address comments

* fix unit tests failing

* update

* address comments

* improve ui

* added skeleton

* address comments

* address feedbacks
This commit is contained in:
Chirag Madlani 2023-03-07 11:05:31 +05:30 committed by GitHub
parent a6048228f4
commit 724a10a82f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1599 additions and 743 deletions

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none"><path fill="#6b7280" 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" width="14" height="14" fill="none"><path fill="#6b7280" 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: 663 B

View File

@ -12,34 +12,40 @@
*/
import { CloseOutlined } from '@ant-design/icons';
import { Divider, Drawer } from 'antd';
import { AxiosError } from 'axios';
import { Col, Drawer, Row, Typography } from 'antd';
import classNames from 'classnames';
import { t } from 'i18next';
import DashboardSummary from 'components/Explore/EntitySummaryPanel/DashboardSummary/DashboardSummary.component';
import MlModelSummary from 'components/Explore/EntitySummaryPanel/MlModelSummary/MlModelSummary.component';
import PipelineSummary from 'components/Explore/EntitySummaryPanel/PipelineSummary/PipelineSummary.component';
import TableSummary from 'components/Explore/EntitySummaryPanel/TableSummary/TableSummary.component';
import TopicSummary from 'components/Explore/EntitySummaryPanel/TopicSummary/TopicSummary.component';
import { FQN_SEPARATOR_CHAR } from 'constants/char.constants';
import { Mlmodel } from 'generated/entity/data/mlmodel';
import React, { useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { getDashboardByFqn } from 'rest/dashboardAPI';
import { getMlModelByFQN } from 'rest/mlModelAPI';
import { getPipelineByFqn } from 'rest/pipelineAPI';
import { getServiceById } from 'rest/serviceAPI';
import { getTableDetailsByFQN } from 'rest/tableAPI';
import { getTopicByFqn } from 'rest/topicsAPI';
import { EntityType } from '../../enums/entity.enum';
import { Dashboard } from '../../generated/entity/data/dashboard';
import { Pipeline } from '../../generated/entity/data/pipeline';
import { Table } from '../../generated/entity/data/table';
import { Topic } from '../../generated/entity/data/topic';
import { getHeaderLabel } from '../../utils/EntityLineageUtils';
import { getEntityOverview, getEntityTags } from '../../utils/EntityUtils';
import {
DRAWER_NAVIGATION_OPTIONS,
getEntityTags,
} from '../../utils/EntityUtils';
import { getEncodedFqn } from '../../utils/StringsUtils';
import { getEntityIcon } from '../../utils/TableUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import RichTextEditorPreviewer from '../common/rich-text-editor/RichTextEditorPreviewer';
import { SelectedNode } from '../EntityLineage/EntityLineage.interface';
import Loader from '../Loader/Loader';
import TagsViewer from '../Tag/TagsViewer/tags-viewer';
import { LineageDrawerProps } from './EntityInfoDrawer.interface';
import './EntityInfoDrawer.style.less';
type EntityData = Table | Pipeline | Dashboard | Topic;
type EntityData = Table | Pipeline | Dashboard | Topic | Mlmodel;
const EntityInfoDrawer = ({
show,
@ -47,10 +53,10 @@ const EntityInfoDrawer = ({
selectedNode,
isMainNode = false,
}: LineageDrawerProps) => {
const { t } = useTranslation();
const [entityDetail, setEntityDetail] = useState<EntityData>(
{} as EntityData
);
const [serviceType, setServiceType] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
@ -67,12 +73,12 @@ const EntityInfoDrawer = ({
])
.then((res) => {
setEntityDetail(res);
setServiceType(res.serviceType ?? '');
})
.catch((err: AxiosError) => {
.catch(() => {
showErrorToast(
err,
t('server.entity-fetch-error', { entity: selectedNode.name })
t('server.error-selected-node-name-details', {
selectedNodeName: selectedNode.name,
})
);
})
.finally(() => {
@ -83,25 +89,42 @@ const EntityInfoDrawer = ({
}
case EntityType.PIPELINE: {
setIsLoading(true);
getPipelineByFqn(getEncodedFqn(selectedNode.fqn), ['tags', 'owner'])
getPipelineByFqn(getEncodedFqn(selectedNode.fqn), [
'tags',
'owner',
'followers',
'tasks',
'tier',
])
.then((res) => {
getServiceById('pipelineServices', res.service?.id)
.then((serviceRes) => {
setServiceType(serviceRes.serviceType ?? '');
})
.catch((err: AxiosError) => {
showErrorToast(
err,
t('server.entity-fetch-error', { entity: selectedNode.name })
);
});
setEntityDetail(res);
setIsLoading(false);
})
.catch((err: AxiosError) => {
.catch(() => {
showErrorToast(
err,
t('server.entity-fetch-error', { entity: selectedNode.name })
t('server.error-selected-node-name-details', {
selectedNodeName: selectedNode.name,
})
);
})
.finally(() => {
setIsLoading(false);
});
break;
}
case EntityType.TOPIC: {
setIsLoading(true);
getTopicByFqn(selectedNode.fqn ?? '', ['tags', 'owner'])
.then((res) => {
setEntityDetail(res);
})
.catch(() => {
showErrorToast(
t('server.error-selected-node-name-details', {
selectedNodeName: selectedNode.name,
})
);
})
.finally(() => {
@ -112,25 +135,20 @@ const EntityInfoDrawer = ({
}
case EntityType.DASHBOARD: {
setIsLoading(true);
getDashboardByFqn(getEncodedFqn(selectedNode.fqn), ['tags', 'owner'])
getDashboardByFqn(getEncodedFqn(selectedNode.fqn), [
'tags',
'owner',
'charts',
])
.then((res) => {
getServiceById('dashboardServices', res.service?.id)
.then((serviceRes) => {
setServiceType(serviceRes.serviceType ?? '');
})
.catch((err: AxiosError) => {
showErrorToast(
err,
t('server.entity-fetch-error', { entity: selectedNode.name })
);
});
setEntityDetail(res);
setIsLoading(false);
})
.catch((err: AxiosError) => {
.catch(() => {
showErrorToast(
err,
t('server.entity-fetch-error', { entity: selectedNode.name })
t('server.error-selected-node-name-details', {
selectedNodeName: selectedNode.name,
})
);
})
.finally(() => {
@ -140,16 +158,97 @@ const EntityInfoDrawer = ({
break;
}
case EntityType.MLMODEL: {
setIsLoading(true);
getMlModelByFQN(getEncodedFqn(selectedNode.fqn), [
'tags',
'owner',
'dashboard',
])
.then((res) => {
setEntityDetail(res);
setIsLoading(false);
})
.catch(() => {
showErrorToast(
t('server.error-selected-node-name-details', {
selectedNodeName: selectedNode.name,
})
);
})
.finally(() => {
setIsLoading(false);
});
break;
}
default:
break;
}
};
const entityInfo = useMemo(
() => getEntityOverview(selectedNode.type, entityDetail, serviceType),
[selectedNode.type, entityDetail, serviceType]
const tags = useMemo(
() => getEntityTags(selectedNode.type, entityDetail),
[entityDetail, selectedNode]
);
const summaryComponent = useMemo(() => {
switch (selectedNode.type) {
case EntityType.TABLE:
return (
<TableSummary
componentType={DRAWER_NAVIGATION_OPTIONS.lineage}
entityDetails={entityDetail as Table}
isLoading={isLoading}
tags={tags}
/>
);
case EntityType.TOPIC:
return (
<TopicSummary
componentType={DRAWER_NAVIGATION_OPTIONS.lineage}
entityDetails={entityDetail as Topic}
isLoading={isLoading}
tags={tags}
/>
);
case EntityType.DASHBOARD:
return (
<DashboardSummary
componentType={DRAWER_NAVIGATION_OPTIONS.lineage}
entityDetails={entityDetail as Dashboard}
isLoading={isLoading}
tags={tags}
/>
);
case EntityType.PIPELINE:
return (
<PipelineSummary
componentType={DRAWER_NAVIGATION_OPTIONS.lineage}
entityDetails={entityDetail as Pipeline}
isLoading={isLoading}
tags={tags}
/>
);
case EntityType.MLMODEL:
return (
<MlModelSummary
componentType={DRAWER_NAVIGATION_OPTIONS.lineage}
entityDetails={entityDetail as Mlmodel}
isLoading={isLoading}
tags={tags}
/>
);
default:
return null;
}
}, [entityDetail, fetchEntityDetail, tags, selectedNode]);
useEffect(() => {
fetchEntityDetail(selectedNode);
}, [selectedNode]);
@ -157,88 +256,45 @@ const EntityInfoDrawer = ({
return (
<Drawer
destroyOnClose
bodyStyle={{ padding: 16 }}
className="entity-panel-container"
closable={false}
extra={<CloseOutlined onClick={onCancel} />}
extra={
<CloseOutlined
data-testid="entity-panel-close-icon"
onClick={onCancel}
/>
}
getContainer={false}
headerStyle={{ padding: 16 }}
mask={false}
open={show}
style={{ position: 'absolute' }}
title={
<p className="tw-flex">
<span className="tw-mr-2">{getEntityIcon(selectedNode.type)}</span>
{getHeaderLabel(
selectedNode.displayName ?? selectedNode.name,
selectedNode.fqn,
selectedNode.type,
isMainNode
)}
</p>
}
visible={show}>
{isLoading ? (
<Loader />
) : (
<>
<section className="tw-mt-1">
<div className="tw-flex tw-flex-col">
{entityInfo.map((d) => {
return (
<div className="tw-py-1.5 tw-flex" key={d.name}>
{d.name && <span>{d.name}:</span>}
<span
className={classNames(
{ 'tw-ml-2': d.name },
{
'link-text': d.isLink,
}
)}>
{d.isLink ? (
<Link
target={d.isExternal ? '_blank' : '_self'}
to={{ pathname: d.url }}>
{d.value}
</Link>
) : (
d.value
)}
</span>
</div>
);
})}
</div>
</section>
{entityInfo.length > 0 && <Divider />}
<section className="tw-mt-1">
<span className="tw-text-grey-muted">{t('label.tag-plural')}</span>
<div className="tw-flex tw-flex-wrap tw-pt-1.5">
{getEntityTags(selectedNode.type, entityDetail).length > 0 ? (
<TagsViewer
sizeCap={-1}
tags={getEntityTags(selectedNode.type, entityDetail)}
/>
) : (
<p className="tw-text-xs tw-text-grey-muted">
{t('label.no-tags-added')}
</p>
<Row gutter={[0, isMainNode ? 6 : 0]}>
<Col span={24}>
{'databaseSchema' in entityDetail && 'database' in entityDetail && (
<span
className="text-grey-muted text-xs"
data-testid="database-schema">{`${entityDetail.database?.name}${FQN_SEPARATOR_CHAR}${entityDetail.databaseSchema?.name}`}</span>
)}
</Col>
<Col span={24}>
<Typography
className={classNames('flex items-center text-base', {
'entity-info-header-link': !isMainNode,
})}>
<span className="m-r-xs">{getEntityIcon(selectedNode.type)}</span>
{getHeaderLabel(
selectedNode.displayName ?? selectedNode.name,
selectedNode.fqn,
selectedNode.type,
isMainNode
)}
</div>
</section>
<Divider />
<section className="tw-mt-1">
<span className="tw-text-grey-muted">{t('label.description')}</span>
<div>
{entityDetail.description?.trim() ? (
<RichTextEditorPreviewer markdown={entityDetail.description} />
) : (
<p className="tw-text-xs tw-text-grey-muted">
{t('label.no-description')}
</p>
)}
</div>
</section>
</>
)}
</Typography>
</Col>
</Row>
}>
{summaryComponent}
</Drawer>
);
};

View File

@ -20,3 +20,39 @@
height: 200px;
}
}
.entity-panel-container {
.ant-drawer-body {
padding: unset;
}
.entity-info-header-link {
&:hover {
color: @primary-color;
.ant-btn-link > span {
color: @primary-color;
}
}
.ant-btn-link > span {
color: @text-color-secondary;
}
}
.summary-panel-statistics-count {
color: @text-color-secondary;
font-size: 18px;
line-height: 24px;
font-weight: 700;
}
.success {
color: @success-color;
}
.failed {
color: @failed-color;
}
.aborted {
color: @aborted-color;
}
}

View File

@ -11,28 +11,43 @@
* limitations under the License.
*/
import { Col, Divider, Row, Space, Typography } from 'antd';
import { Col, Divider, Row, Typography } from 'antd';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import SummaryTagsDescription from 'components/common/SummaryTagsDescription/SummaryTagsDescription.component';
import SummaryPanelSkeleton from 'components/Skeleton/SummaryPanelSkeleton/SummaryPanelSkeleton.component';
import { ExplorePageTabs } from 'enums/Explore.enum';
import { TagLabel } from 'generated/type/tagLabel';
import { ChartType } from 'pages/DashboardDetailsPage/DashboardDetailsPage.component';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
DRAWER_NAVIGATION_OPTIONS,
getEntityOverview,
} from 'utils/EntityUtils';
import SVGIcons from 'utils/SvgUtils';
import { SummaryEntityType } from '../../../../enums/EntitySummary.enum';
import { SearchIndex } from '../../../../enums/search.enum';
import { Dashboard } from '../../../../generated/entity/data/dashboard';
import { fetchCharts } from '../../../../utils/DashboardDetailsUtils';
import { getFormattedEntityData } from '../../../../utils/EntitySummaryPanelUtils';
import SVGIcons from '../../../../utils/SvgUtils';
import { showErrorToast } from '../../../../utils/ToastUtils';
import TableDataCardTitle from '../../../common/table-data-card-v2/TableDataCardTitle.component';
import SummaryList from '../SummaryList/SummaryList.component';
import { BasicEntityInfo } from '../SummaryList/SummaryList.interface';
interface DashboardSummaryProps {
entityDetails: Dashboard;
componentType?: string;
tags?: TagLabel[];
isLoading?: boolean;
}
function DashboardSummary({ entityDetails }: DashboardSummaryProps) {
function DashboardSummary({
entityDetails,
componentType = DRAWER_NAVIGATION_OPTIONS.explore,
tags,
isLoading,
}: DashboardSummaryProps) {
const { t } = useTranslation();
const [charts, setCharts] = useState<ChartType[]>();
@ -56,6 +71,11 @@ function DashboardSummary({ entityDetails }: DashboardSummaryProps) {
}
};
const entityInfo = useMemo(
() => getEntityOverview(ExplorePageTabs.DASHBOARDS, entityDetails),
[entityDetails]
);
useEffect(() => {
fetchChartsDetails();
}, [entityDetails]);
@ -65,63 +85,96 @@ function DashboardSummary({ entityDetails }: DashboardSummaryProps) {
[charts]
);
const isExplore = useMemo(
() => componentType === DRAWER_NAVIGATION_OPTIONS.explore,
[componentType]
);
return (
<>
<Row className="m-md" gutter={[0, 4]}>
<Col span={24}>
<TableDataCardTitle
dataTestId="summary-panel-title"
searchIndex={SearchIndex.DASHBOARD}
source={entityDetails}
/>
</Col>
<Col span={24}>
<Row gutter={16}>
<Col
className="text-gray"
data-testid="dashboard-url-label"
span={10}>
{`${t('label.dashboard')} ${t('label.url-uppercase')}`}
</Col>
<Col data-testid="dashboard-url-value" span={12}>
{entityDetails.dashboardUrl ? (
<Link
target="_blank"
to={{ pathname: entityDetails.dashboardUrl }}>
<Space align="start">
<Typography.Text
className="link"
data-testid="dashboard-link-name">
{entityDetails.name}
</Typography.Text>
<SVGIcons
alt="external-link"
icon="external-link"
width="12px"
/>
</Space>
</Link>
) : (
'-'
)}
</Col>
</Row>
</Col>
</Row>
<Divider className="m-0" />
<Row className="m-md" gutter={[0, 16]}>
<Col span={24}>
<Typography.Text
className="section-header"
data-testid="charts-header">
{t('label.chart-plural')}
</Typography.Text>
</Col>
<Col span={24}>
<SummaryList formattedEntityData={formattedChartsData} />
</Col>
</Row>
</>
<SummaryPanelSkeleton loading={Boolean(isLoading)}>
<>
<Row className="m-md" gutter={[0, 4]}>
<Col span={24}>
<Row>
{entityInfo.map((info) => {
const isOwner = info.name === t('label.owner');
return info.visible?.includes(componentType) ? (
<Col key={info.name} span={24}>
<Row
className={classNames('', {
'p-b-md': isOwner,
})}
gutter={[16, 32]}>
{!isOwner ? (
<Col data-testid={`${info.name}-label`} span={8}>
<Typography.Text className="text-grey-muted">
{info.name}
</Typography.Text>
</Col>
) : null}
<Col data-testid="dashboard-url-value" span={16}>
{info.isLink ? (
<Link
component={Typography.Link}
target={info.isExternal ? '_blank' : '_self'}
to={{ pathname: info.url }}>
<Typography.Link
className="text-primary"
data-testid="dashboard-link-name">
{info.value}
</Typography.Link>
{info.isExternal ? (
<SVGIcons
alt="external-link"
className="m-l-xs"
icon="external-link"
width="12px"
/>
) : null}
</Link>
) : (
<Typography.Text
className={classNames('text-grey-muted', {
'text-grey-body': !isOwner,
})}>
{info.value}
</Typography.Text>
)}
</Col>
</Row>
</Col>
) : null;
})}
</Row>
</Col>
</Row>
<Divider className="m-y-xs" />
{!isExplore ? (
<>
<SummaryTagsDescription
entityDetail={entityDetails}
tags={tags ? tags : []}
/>
<Divider className="m-y-xs" />
</>
) : null}
<Row className="m-md" gutter={[0, 16]}>
<Col span={24}>
<Typography.Text
className="text-base text-grey-muted"
data-testid="charts-header">
{t('label.chart-plural')}
</Typography.Text>
</Col>
<Col span={24}>
<SummaryList formattedEntityData={formattedChartsData} />
</Col>
</Row>
</>
</SummaryPanelSkeleton>
);
}

View File

@ -14,22 +14,13 @@
import { act, render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { DRAWER_NAVIGATION_OPTIONS } from 'utils/EntityUtils';
import {
mockDashboardEntityDetails,
mockFetchChartsResponse,
} from '../mocks/DashboardSummary.mock';
import DashboardSummary from './DashboardSummary.component';
jest.mock(
'../../../common/table-data-card-v2/TableDataCardTitle.component',
() =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="TableDataCardTitle">TableDataCardTitle</div>
))
);
jest.mock('../SummaryList/SummaryList.component', () =>
jest
.fn()
@ -48,19 +39,68 @@ describe('DashboardSummary component tests', () => {
});
});
const dashboardTitle = screen.getByTestId('TableDataCardTitle');
const dashboardUrlLabel = screen.getByTestId('dashboard-url-label');
const dashboardUrlValue = screen.getByTestId('dashboard-link-name');
const dashboardUrlLabel = screen.getByText(
'label.dashboard label.url-uppercase'
);
const dashboardUrlValue = screen.getByTestId('dashboard-url-value');
const dashboardLinkName = screen.getByTestId('dashboard-link-name');
const chartsHeader = screen.getByTestId('charts-header');
const summaryList = screen.getByTestId('SummaryList');
expect(dashboardTitle).toBeInTheDocument();
expect(dashboardLinkName).toBeInTheDocument();
expect(dashboardUrlLabel).toBeInTheDocument();
expect(dashboardUrlValue).toContainHTML(mockDashboardEntityDetails.name);
expect(chartsHeader).toBeInTheDocument();
expect(summaryList).toBeInTheDocument();
});
it('Component should render properly, when loaded in the Lineage page.', async () => {
const labels = ['label.service-label', 'label.tier-label'];
await act(async () => {
const { debug } = render(
<DashboardSummary
componentType={DRAWER_NAVIGATION_OPTIONS.lineage}
entityDetails={mockDashboardEntityDetails}
/>,
{
wrapper: MemoryRouter,
}
);
debug();
});
const dashboardUrlLabel = screen.getByText(
'label.dashboard label.url-uppercase'
);
const ownerLabel = screen.queryByTestId('label.owner-label');
const dashboardUrl = screen.getAllByTestId('dashboard-url-value');
const tags = screen.getByText('label.tag-plural');
const description = screen.getByText('label.description');
const noDataFound = screen.getByText('label.no-data-found');
const dashboardLink = screen.getAllByTestId('dashboard-link-name');
const dashboardValue = screen.getAllByTestId('dashboard-url-value');
labels.forEach((label) =>
expect(screen.getByTestId(label)).toBeInTheDocument()
);
expect(ownerLabel).not.toBeInTheDocument();
expect(dashboardUrl[0]).toBeInTheDocument();
expect(dashboardLink[0]).toBeInTheDocument();
expect(dashboardValue[0]).toBeInTheDocument();
expect(tags).toBeInTheDocument();
expect(description).toBeInTheDocument();
expect(noDataFound).toBeInTheDocument();
expect(dashboardUrlLabel).toBeInTheDocument();
});
it('If the dashboard url is not present in dashboard details, "-" should be displayed as dashboard url value', async () => {
await act(async () => {
render(

View File

@ -12,8 +12,10 @@
*/
import { CloseOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import React, { useMemo } from 'react';
import { Col, Drawer, Row } from 'antd';
import TableDataCardTitle from 'components/common/table-data-card-v2/TableDataCardTitle.component';
import { EntityType } from 'enums/entity.enum';
import React, { useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { ExplorePageTabs } from '../../../enums/Explore.enum';
import { Dashboard } from '../../../generated/entity/data/dashboard';
@ -34,16 +36,23 @@ export default function EntitySummaryPanel({
handleClosePanel,
}: EntitySummaryPanelProps) {
const { tab } = useParams<{ tab: string }>();
const [currentSearchIndex, setCurrentSearchIndex] = useState<EntityType>();
const summaryComponent = useMemo(() => {
switch (entityDetails.entityType) {
case ExplorePageTabs.TABLES:
setCurrentSearchIndex(EntityType.TABLE);
return <TableSummary entityDetails={entityDetails.details as Table} />;
case ExplorePageTabs.TOPICS:
setCurrentSearchIndex(EntityType.TOPIC);
return <TopicSummary entityDetails={entityDetails.details as Topic} />;
case ExplorePageTabs.DASHBOARDS:
setCurrentSearchIndex(EntityType.DASHBOARD);
return (
<DashboardSummary
entityDetails={entityDetails.details as Dashboard}
@ -51,11 +60,15 @@ export default function EntitySummaryPanel({
);
case ExplorePageTabs.PIPELINES:
setCurrentSearchIndex(EntityType.PIPELINE);
return (
<PipelineSummary entityDetails={entityDetails.details as Pipeline} />
);
case ExplorePageTabs.MLMODELS:
setCurrentSearchIndex(EntityType.MLMODEL);
return (
<MlModelSummary entityDetails={entityDetails.details as Mlmodel} />
);
@ -66,13 +79,34 @@ export default function EntitySummaryPanel({
}, [tab, entityDetails]);
return (
<div className={classNames('summary-panel-container')}>
<Drawer
destroyOnClose
open
className="summary-panel-container"
closable={false}
extra={
<CloseOutlined
data-testid="summary-panel-close-icon"
onClick={handleClosePanel}
/>
}
getContainer={false}
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>
}
width="100%">
{summaryComponent}
<CloseOutlined
className="close-icon"
data-testid="summary-panel-close-icon"
onClick={handleClosePanel}
/>
</div>
</Drawer>
);
}

View File

@ -33,31 +33,25 @@
-ms-overflow-style: none;
scrollbar-width: none;
.ant-typography {
color: #37352f;
.ant-drawer-content-wrapper {
box-shadow: -2px 2px 4px rgba(0, 0, 0, 0.12);
}
.ant-drawer-body {
padding: unset;
}
.summary-panel-statistics-count {
color: #37352f;
color: @text-color-secondary;
font-size: 18px;
line-height: 24px;
font-weight: 700;
}
.text-gray {
color: @label-color;
}
.section-header {
font-size: 16px;
color: @section-header-color;
}
.success {
color: @successColor;
}
.failed {
color: @failedColor;
color: @failed-color;
}
.aborted {
color: @abortedColor;

View File

@ -64,6 +64,16 @@ 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' })),
}));
@ -80,9 +90,11 @@ describe('EntitySummaryPanel component tests', () => {
/>
);
const tableDataCardTitle = screen.getByText('TableDataCardTitle');
const tableSummary = screen.getByTestId('TableSummary');
const closeIcon = screen.getByTestId('summary-panel-close-icon');
expect(tableDataCardTitle).toBeInTheDocument();
expect(tableSummary).toBeInTheDocument();
expect(closeIcon).toBeInTheDocument();

View File

@ -12,50 +12,42 @@
*/
import { Col, Divider, Row, Typography } from 'antd';
import { startCase } from 'lodash';
import React, { ReactNode, useMemo } from 'react';
import classNames from 'classnames';
import SummaryTagsDescription from 'components/common/SummaryTagsDescription/SummaryTagsDescription.component';
import SummaryPanelSkeleton from 'components/Skeleton/SummaryPanelSkeleton/SummaryPanelSkeleton.component';
import { ExplorePageTabs } from 'enums/Explore.enum';
import { TagLabel } from 'generated/type/tagLabel';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { getDashboardDetailsPath } from '../../../../constants/constants';
import {
DRAWER_NAVIGATION_OPTIONS,
getEntityOverview,
} from 'utils/EntityUtils';
import SVGIcons from 'utils/SvgUtils';
import { SummaryEntityType } from '../../../../enums/EntitySummary.enum';
import { SearchIndex } from '../../../../enums/search.enum';
import { Mlmodel } from '../../../../generated/entity/data/mlmodel';
import { getEntityName } from '../../../../utils/CommonUtils';
import { getFormattedEntityData } from '../../../../utils/EntitySummaryPanelUtils';
import TableDataCardTitle from '../../../common/table-data-card-v2/TableDataCardTitle.component';
import SummaryList from '../SummaryList/SummaryList.component';
import { BasicEntityInfo } from '../SummaryList/SummaryList.interface';
interface MlModelSummaryProps {
entityDetails: Mlmodel;
componentType?: string;
tags?: TagLabel[];
isLoading?: boolean;
}
interface BasicMlModelInfo {
algorithm: string;
target?: string;
server?: ReactNode;
dashboard?: ReactNode;
}
function MlModelSummary({ entityDetails }: MlModelSummaryProps) {
function MlModelSummary({
entityDetails,
componentType = DRAWER_NAVIGATION_OPTIONS.explore,
tags,
isLoading,
}: MlModelSummaryProps) {
const { t } = useTranslation();
const basicMlModelInfo: BasicMlModelInfo = useMemo(
() => ({
algorithm: entityDetails.algorithm,
target: entityDetails.target,
server: entityDetails.server ? (
<a href={entityDetails.server}>{entityDetails.server}</a>
) : undefined,
dashboard: entityDetails.dashboard ? (
<Link
to={getDashboardDetailsPath(
entityDetails.dashboard?.fullyQualifiedName as string
)}>
{getEntityName(entityDetails.dashboard)}
</Link>
) : undefined,
}),
const entityInfo = useMemo(
() => getEntityOverview(ExplorePageTabs.MLMODELS, entityDetails),
[entityDetails]
);
@ -68,55 +60,91 @@ function MlModelSummary({ entityDetails }: MlModelSummaryProps) {
[entityDetails]
);
return (
<>
<Row className="m-md" gutter={[0, 4]}>
<Col span={24}>
<TableDataCardTitle
dataTestId="summary-panel-title"
searchIndex={SearchIndex.MLMODEL}
source={entityDetails}
/>
</Col>
<Col span={24}>
<Row>
{Object.keys(basicMlModelInfo).map((fieldName) => {
const value =
basicMlModelInfo[fieldName as keyof BasicMlModelInfo];
const isExplore = useMemo(
() => componentType === DRAWER_NAVIGATION_OPTIONS.explore,
[componentType]
);
return (
<Col key={fieldName} span={24}>
<Row gutter={16}>
<Col
className="text-gray"
data-testid={`${fieldName}-label`}
span={10}>
{startCase(fieldName)}
</Col>
<Col data-testid={`${fieldName}-value`} span={12}>
{value ? value : '-'}
</Col>
</Row>
</Col>
);
})}
</Row>
</Col>
</Row>
<Divider className="m-0" />
<Row className="m-md" gutter={[0, 16]}>
<Col span={24}>
<Typography.Text
className="section-header"
data-testid="features-header">
{t('label.feature-plural')}
</Typography.Text>
</Col>
<Col span={24}>
<SummaryList formattedEntityData={formattedFeaturesData} />
</Col>
</Row>
</>
return (
<SummaryPanelSkeleton loading={Boolean(isLoading)}>
<>
<Row className="m-md" gutter={[0, 4]}>
<Col span={24}>
<Row>
{entityInfo.map((info) => {
const isOwner = info.name === t('label.owner');
return info.visible?.includes(componentType) ? (
<Col key={info.name} span={24}>
<Row
className={classNames('', {
'p-b-md': isOwner,
})}
gutter={[16, 32]}>
{!isOwner ? (
<Col data-testid={`${info.name}-label`} span={8}>
<Typography.Text className="text-grey-muted">
{info.name}
</Typography.Text>
</Col>
) : null}
<Col data-testid={`${info.name}-value`} span={16}>
{info.isLink ? (
<Link
target={info.isExternal ? '_blank' : '_self'}
to={{ pathname: info.url }}>
{info.value}
{info.isExternal ? (
<SVGIcons
alt="external-link"
className="m-l-xs"
icon="external-link"
width="12px"
/>
) : null}
</Link>
) : (
<Typography.Text
className={classNames('text-grey-muted', {
'text-grey-body': !isOwner,
})}>
{info.value}
</Typography.Text>
)}
</Col>
</Row>
</Col>
) : null;
})}
</Row>
</Col>
</Row>
<Divider className="m-y-xs" />
{!isExplore ? (
<>
<SummaryTagsDescription
entityDetail={entityDetails}
tags={tags ? tags : []}
/>
<Divider className="m-y-xs" />
</>
) : null}
<Row className="m-md" gutter={[0, 16]}>
<Col span={24}>
<Typography.Text
className="text-base text-grey-muted"
data-testid="features-header">
{t('label.feature-plural')}
</Typography.Text>
</Col>
<Col span={24}>
<SummaryList formattedEntityData={formattedFeaturesData} />
</Col>
</Row>
</>
</SummaryPanelSkeleton>
);
}

View File

@ -20,16 +20,6 @@ import {
} from '../mocks/MlModelSummary.mock';
import MlModelSummary from './MlModelSummary.component';
jest.mock(
'../../../common/table-data-card-v2/TableDataCardTitle.component',
() =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="TableDataCardTitle">TableDataCardTitle</div>
))
);
jest.mock('../SummaryList/SummaryList.component', () =>
jest
.fn()
@ -42,17 +32,15 @@ describe('MlModelSummary component tests', () => {
wrapper: MemoryRouter,
});
const mlModelTitle = screen.getByTestId('TableDataCardTitle');
const algorithmLabel = screen.getByTestId('algorithm-label');
const targetLabel = screen.getByTestId('target-label');
const serverLabel = screen.getByTestId('server-label');
const dashboardLabel = screen.getByTestId('dashboard-label');
const algorithmValue = screen.getByTestId('algorithm-value');
const targetValue = screen.getByTestId('target-value');
const serverValue = screen.getByTestId('server-value');
const dashboardValue = screen.getByTestId('dashboard-value');
const algorithmLabel = screen.getByTestId('label.algorithm-label');
const targetLabel = screen.getByTestId('label.target-label');
const serverLabel = screen.getByTestId('label.server-label');
const dashboardLabel = screen.getByTestId('label.dashboard-label');
const algorithmValue = screen.getByTestId('label.algorithm-value');
const targetValue = screen.getByTestId('label.target-value');
const serverValue = screen.getByTestId('label.server-value');
const dashboardValue = screen.getByTestId('label.dashboard-value');
expect(mlModelTitle).toBeInTheDocument();
expect(algorithmLabel).toBeInTheDocument();
expect(targetLabel).toBeInTheDocument();
expect(serverLabel).toBeInTheDocument();
@ -68,14 +56,14 @@ describe('MlModelSummary component tests', () => {
wrapper: MemoryRouter,
});
const algorithmLabel = screen.getByTestId('algorithm-label');
const targetLabel = screen.queryByTestId('target-label');
const serverLabel = screen.queryByTestId('server-label');
const dashboardLabel = screen.queryByTestId('dashboard-label');
const algorithmValue = screen.getByTestId('algorithm-value');
const targetValue = screen.getByTestId('target-value');
const serverValue = screen.getByTestId('server-value');
const dashboardValue = screen.getByTestId('dashboard-value');
const algorithmLabel = screen.getByTestId('label.algorithm-label');
const targetLabel = screen.queryByTestId('label.target-label');
const serverLabel = screen.queryByTestId('label.server-label');
const dashboardLabel = screen.queryByTestId('label.dashboard-label');
const algorithmValue = screen.getByTestId('label.algorithm-value');
const targetValue = screen.getByTestId('label.target-value');
const serverValue = screen.getByTestId('label.server-value');
const dashboardValue = screen.getByTestId('label.dashboard-value');
expect(algorithmLabel).toBeInTheDocument();
expect(targetLabel).toBeInTheDocument();

View File

@ -12,23 +12,37 @@
*/
import { Col, Divider, Row, Space, Typography } from 'antd';
import classNames from 'classnames';
import SummaryTagsDescription from 'components/common/SummaryTagsDescription/SummaryTagsDescription.component';
import SummaryPanelSkeleton from 'components/Skeleton/SummaryPanelSkeleton/SummaryPanelSkeleton.component';
import { ExplorePageTabs } from 'enums/Explore.enum';
import { TagLabel } from 'generated/type/tagLabel';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
DRAWER_NAVIGATION_OPTIONS,
getEntityOverview,
} from 'utils/EntityUtils';
import SVGIcons from 'utils/SvgUtils';
import { SummaryEntityType } from '../../../../enums/EntitySummary.enum';
import { SearchIndex } from '../../../../enums/search.enum';
import { Pipeline } from '../../../../generated/entity/data/pipeline';
import { getFormattedEntityData } from '../../../../utils/EntitySummaryPanelUtils';
import SVGIcons from '../../../../utils/SvgUtils';
import TableDataCardTitle from '../../../common/table-data-card-v2/TableDataCardTitle.component';
import SummaryList from '../SummaryList/SummaryList.component';
import { BasicEntityInfo } from '../SummaryList/SummaryList.interface';
interface PipelineSummaryProps {
entityDetails: Pipeline;
componentType?: string;
tags?: TagLabel[];
isLoading?: boolean;
}
function PipelineSummary({ entityDetails }: PipelineSummaryProps) {
function PipelineSummary({
entityDetails,
componentType = DRAWER_NAVIGATION_OPTIONS.explore,
tags,
isLoading,
}: PipelineSummaryProps) {
const { t } = useTranslation();
const formattedTasksData: BasicEntityInfo[] = useMemo(
@ -36,63 +50,105 @@ function PipelineSummary({ entityDetails }: PipelineSummaryProps) {
[entityDetails]
);
const entityInfo = useMemo(
() => getEntityOverview(ExplorePageTabs.PIPELINES, entityDetails),
[entityDetails]
);
const isExplore = useMemo(
() => componentType === DRAWER_NAVIGATION_OPTIONS.explore,
[componentType]
);
return (
<>
<Row className="m-md" gutter={[0, 4]}>
<Col span={24}>
<TableDataCardTitle
dataTestId="summary-panel-title"
searchIndex={SearchIndex.PIPELINE}
source={entityDetails}
/>
</Col>
<Col span={24}>
<Row gutter={16}>
<Col
className="text-gray"
data-testid="pipeline-url-label"
span={10}>
{`${t('label.pipeline')} ${t('label.url-uppercase')}`}
</Col>
<Col data-testid="pipeline-url-value" span={12}>
{entityDetails.pipelineUrl ? (
<Link
target="_blank"
to={{ pathname: entityDetails.pipelineUrl }}>
<Space align="start">
<Typography.Text
className="link"
data-testid="pipeline-link-name">
{entityDetails.name}
</Typography.Text>
<SVGIcons
alt="external-link"
icon="external-link"
width="12px"
/>
</Space>
</Link>
) : (
'-'
)}
</Col>
</Row>
</Col>
</Row>
<Divider className="m-0" />
<Row className="m-md" gutter={[0, 16]}>
<Col span={24}>
<Typography.Text
className="section-header"
data-testid="tasks-header">
{t('label.task-plural')}
</Typography.Text>
</Col>
<Col span={24}>
<SummaryList formattedEntityData={formattedTasksData} />
</Col>
</Row>
</>
<SummaryPanelSkeleton loading={Boolean(isLoading)}>
<>
<Row className="m-md" gutter={[0, 4]}>
<Col span={24}>
<Row>
{entityInfo.map((info) => {
const isOwner = info.name === t('label.owner');
return info.visible?.includes(componentType) ? (
<Col key={info.name} span={24}>
<Row
className={classNames('', {
'p-b-md': isOwner,
})}
gutter={[16, 32]}>
{!isOwner ? (
<Col
data-testid={
info.dataTestId
? info.dataTestId
: `${info.name}-label`
}
span={8}>
<Typography.Text className="text-grey-muted">
{info.name}
</Typography.Text>
</Col>
) : null}
<Col data-testid={`${info.name}-value`} span={16}>
{info.isLink ? (
<Space align="start">
<Typography.Link
data-testid="pipeline-link-name"
href={info.url}
target={info.isExternal ? '_blank' : '_self'}>
{info.value}
{info.isExternal ? (
<SVGIcons
alt="external-link"
className="m-l-xs"
icon="external-link"
width="12px"
/>
) : null}
</Typography.Link>
</Space>
) : (
<Typography.Text
className={classNames('text-grey-muted', {
'text-grey-body': !isOwner,
})}>
{info.value}
</Typography.Text>
)}
</Col>
</Row>
</Col>
) : null;
})}
</Row>
</Col>
</Row>
<Divider className="m-y-xs" />
{!isExplore ? (
<>
<SummaryTagsDescription
entityDetail={entityDetails}
tags={tags ? tags : []}
/>
<Divider className="m-y-xs" />
</>
) : null}
<Row className="m-md" gutter={[0, 16]}>
<Col span={24}>
<Typography.Text
className="text-base text-grey-muted"
data-testid="tasks-header">
{t('label.task-plural')}
</Typography.Text>
</Col>
<Col span={24}>
<SummaryList formattedEntityData={formattedTasksData} />
</Col>
</Row>
</>
</SummaryPanelSkeleton>
);
}

View File

@ -14,19 +14,10 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { DRAWER_NAVIGATION_OPTIONS } from 'utils/EntityUtils';
import { mockPipelineEntityDetails } from '../mocks/PipelineSummary.mock';
import PipelineSummary from './PipelineSummary.component';
jest.mock(
'../../../common/table-data-card-v2/TableDataCardTitle.component',
() =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="TableDataCardTitle">TableDataCardTitle</div>
))
);
jest.mock('../SummaryList/SummaryList.component', () =>
jest
.fn()
@ -34,24 +25,72 @@ jest.mock('../SummaryList/SummaryList.component', () =>
);
describe('PipelineSummary component tests', () => {
it('Component should render properly', () => {
it('Component should render properly, when loaded in the Explore page.', () => {
render(<PipelineSummary entityDetails={mockPipelineEntityDetails} />, {
wrapper: MemoryRouter,
});
const pipelineTitle = screen.getByTestId('TableDataCardTitle');
const pipelineUrlLabel = screen.getByTestId('pipeline-url-label');
const pipelineUrlValue = screen.getByTestId('pipeline-link-name');
const tasksHeader = screen.getByTestId('tasks-header');
const summaryList = screen.getByTestId('SummaryList');
expect(pipelineTitle).toBeInTheDocument();
expect(pipelineUrlLabel).toBeInTheDocument();
expect(pipelineUrlValue).toContainHTML(mockPipelineEntityDetails.name);
expect(tasksHeader).toBeInTheDocument();
expect(summaryList).toBeInTheDocument();
});
it('Component should render properly, when loaded in the Lineage page.', async () => {
const labels = [
'pipeline-url-label',
'label.pipeline label.url-uppercase-value',
'label.service-label',
'label.tier-label',
];
const values = [
'label.service-value',
'label.owner-value',
'label.tier-value',
];
render(
<PipelineSummary
componentType={DRAWER_NAVIGATION_OPTIONS.lineage}
entityDetails={mockPipelineEntityDetails}
/>,
{
wrapper: MemoryRouter,
}
);
const schemaHeader = screen.getAllByTestId('schema-header');
const tags = screen.getByText('label.tag-plural');
const noTags = screen.getByText('label.no-tags-added');
const pipelineName = screen.getAllByTestId('pipeline-link-name');
const viewerContainer = screen.getByTestId('viewer-container');
const summaryList = screen.getByTestId('SummaryList');
const ownerLabel = screen.queryByTestId('label.owner-label');
labels.forEach((label) =>
expect(screen.getByTestId(label)).toBeInTheDocument()
);
values.forEach((value) =>
expect(screen.getByTestId(value)).toBeInTheDocument()
);
expect(ownerLabel).not.toBeInTheDocument();
expect(schemaHeader[0]).toBeInTheDocument();
expect(tags).toBeInTheDocument();
expect(pipelineName[0]).toBeInTheDocument();
expect(noTags).toBeInTheDocument();
expect(summaryList).toBeInTheDocument();
expect(viewerContainer).toBeInTheDocument();
});
it('If the pipeline url is not present in pipeline details, "-" should be displayed as pipeline url value', () => {
render(
<PipelineSummary
@ -62,7 +101,9 @@ describe('PipelineSummary component tests', () => {
}
);
const pipelineUrlValue = screen.getByTestId('pipeline-url-value');
const pipelineUrlValue = screen.getByTestId(
'label.pipeline label.url-uppercase-value'
);
expect(pipelineUrlValue).toContainHTML('-');
});

View File

@ -32,7 +32,9 @@ export default function SummaryList({
<Row>
{isEmpty(formattedEntityData) ? (
<div className="m-y-md">
<Text className="text-gray">{t('message.no-data-available')}</Text>
<Text className="text-grey-body">
{t('message.no-data-available')}
</Text>
</div>
) : (
formattedEntityData.map((entity) =>

View File

@ -49,8 +49,12 @@ function SummaryListItem({
<Col>
{entityDetails.type && (
<Space size={4}>
<Text className="text-gray">{`${t('label.type')}:`}</Text>
<Text className="font-medium" data-testid="entity-type">
<Text className="text-grey-muted">{`${t(
'label.type'
)}:`}</Text>
<Text
className="font-medium text-grey-body"
data-testid="entity-type">
{entityDetails.type}
</Text>
</Space>
@ -64,10 +68,12 @@ function SummaryListItem({
</Col>
<Col>
<Space size={4}>
<Text className="text-gray">{`${t(
<Text className="text-grey-muted">{`${t(
'label.algorithm'
)}:`}</Text>
<Text className="font-medium" data-testid="algorithm">
<Text
className="font-medium text-grey-body"
data-testid="algorithm">
{entityDetails.algorithm}
</Text>
</Space>
@ -99,8 +105,8 @@ function SummaryListItem({
)}
</Row>
</Col>
<Col span={24}>
<Paragraph>
<Col className="m-t-md" span={24}>
<Paragraph className="text-grey-body">
{entityDetails.description ? (
<RichTextEditorPreviewer
markdown={entityDetails.description || ''}
@ -112,7 +118,7 @@ function SummaryListItem({
</Paragraph>
</Col>
</Row>
<Divider />
<Divider className="m-y-xs" />
</Col>
);
}

View File

@ -14,38 +14,56 @@
import { Col, Divider, Row, Typography } from 'antd';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import SummaryTagsDescription from 'components/common/SummaryTagsDescription/SummaryTagsDescription.component';
import SummaryPanelSkeleton from 'components/Skeleton/SummaryPanelSkeleton/SummaryPanelSkeleton.component';
import { ExplorePageTabs } from 'enums/Explore.enum';
import { isEmpty, isUndefined } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import {
default as React,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
getLatestTableProfileByFqn,
getTableQueryByTableId,
} from 'rest/tableAPI';
import { getListTestCase } from 'rest/testAPI';
import {
DRAWER_NAVIGATION_OPTIONS,
getEntityOverview,
} from 'utils/EntityUtils';
import SVGIcons from 'utils/SvgUtils';
import { API_RES_MAX_SIZE } from '../../../../constants/constants';
import { INITIAL_TEST_RESULT_SUMMARY } from '../../../../constants/profiler.constant';
import { SummaryEntityType } from '../../../../enums/EntitySummary.enum';
import { SearchIndex } from '../../../../enums/search.enum';
import { Table, TableType } from '../../../../generated/entity/data/table';
import { Table } from '../../../../generated/entity/data/table';
import { Include } from '../../../../generated/type/include';
import {
formatNumberWithComma,
formTwoDigitNmber,
formTwoDigitNmber as formTwoDigitNumber,
} from '../../../../utils/CommonUtils';
import { updateTestResults } from '../../../../utils/DataQualityAndProfilerUtils';
import { getFormattedEntityData } from '../../../../utils/EntitySummaryPanelUtils';
import { generateEntityLink } from '../../../../utils/TableUtils';
import { showErrorToast } from '../../../../utils/ToastUtils';
import TableDataCardTitle from '../../../common/table-data-card-v2/TableDataCardTitle.component';
import {
OverallTableSummeryType,
TableTestsType,
} from '../../../TableProfiler/TableProfiler.interface';
import SummaryList from '../SummaryList/SummaryList.component';
import { BasicEntityInfo } from '../SummaryList/SummaryList.interface';
import { BasicTableInfo, TableSummaryProps } from './TableSummary.interface';
import { TableSummaryProps } from './TableSummary.interface';
function TableSummary({ entityDetails }: TableSummaryProps) {
function TableSummary({
entityDetails,
componentType = DRAWER_NAVIGATION_OPTIONS.explore,
tags,
isLoading,
}: TableSummaryProps) {
const { t } = useTranslation();
const [tableDetails, setTableDetails] = useState<Table>(entityDetails);
const [tableTests, setTableTests] = useState<TableTestsType>({
@ -53,6 +71,11 @@ function TableSummary({ entityDetails }: TableSummaryProps) {
results: INITIAL_TEST_RESULT_SUMMARY,
});
const isExplore = useMemo(
() => componentType === DRAWER_NAVIGATION_OPTIONS.explore,
[componentType]
);
const isTableDeleted = useMemo(() => entityDetails.deleted, [entityDetails]);
const fetchAllTests = async () => {
@ -84,13 +107,13 @@ function TableSummary({ entityDetails }: TableSummaryProps) {
}
};
const fetchProfilerData = async () => {
const fetchProfilerData = useCallback(async () => {
try {
const profileResponse = await getLatestTableProfileByFqn(
entityDetails?.fullyQualifiedName || ''
);
const { profile } = profileResponse;
const { profile, tableConstraints } = profileResponse;
const queriesResponse = await getTableQueryByTableId(
entityDetails.id || ''
@ -100,7 +123,7 @@ function TableSummary({ entityDetails }: TableSummaryProps) {
setTableDetails((prev) => {
if (prev) {
return { ...prev, profile, tableQueries };
return { ...prev, profile, tableQueries, tableConstraints };
} else {
return {} as Table;
}
@ -113,7 +136,7 @@ function TableSummary({ entityDetails }: TableSummaryProps) {
})
);
}
};
}, [entityDetails]);
const overallSummary: OverallTableSummeryType[] | undefined = useMemo(() => {
if (isUndefined(tableDetails.profile)) {
@ -142,31 +165,27 @@ function TableSummary({ entityDetails }: TableSummaryProps) {
},
{
title: `${t('label.test-plural')} ${t('label.passed')}`,
value: formTwoDigitNmber(tableTests.results.success),
value: formTwoDigitNumber(tableTests.results.success),
className: 'success',
},
{
title: `${t('label.test-plural')} ${t('label.aborted')}`,
value: formTwoDigitNmber(tableTests.results.aborted),
value: formTwoDigitNumber(tableTests.results.aborted),
className: 'aborted',
},
{
title: `${t('label.test-plural')} ${t('label.failed')}`,
value: formTwoDigitNmber(tableTests.results.failed),
value: formTwoDigitNumber(tableTests.results.failed),
className: 'failed',
},
];
}, [tableDetails, tableTests]);
const { tableType, columns, tableQueries } = tableDetails;
const { columns } = tableDetails;
const basicTableInfo: BasicTableInfo = useMemo(
() => ({
Type: tableType || TableType.Regular,
Queries: tableQueries?.length ? `${tableQueries?.length}` : '-',
Columns: columns?.length ? `${columns?.length}` : '-',
}),
[tableType, columns, tableQueries]
const entityInfo = useMemo(
() => getEntityOverview(ExplorePageTabs.TABLES, tableDetails),
[tableDetails]
);
const formattedColumnsData: BasicEntityInfo[] = useMemo(
@ -182,102 +201,148 @@ function TableSummary({ entityDetails }: TableSummaryProps) {
useEffect(() => {
if (!isEmpty(entityDetails)) {
setTableDetails(entityDetails);
fetchAllTests();
!isTableDeleted && fetchProfilerData();
if (
!isTableDeleted &&
entityDetails.service?.type === 'databaseService'
) {
fetchProfilerData();
fetchAllTests();
}
}
}, [entityDetails]);
return (
<>
<Row className={classNames('m-md')} gutter={[0, 4]}>
<Col span={24}>
<TableDataCardTitle
dataTestId="summary-panel-title"
searchIndex={SearchIndex.TABLE}
source={tableDetails}
/>
</Col>
<Col span={24}>
<Row>
{Object.keys(basicTableInfo).map((fieldName) => (
<Col key={fieldName} span={24}>
<Row gutter={16}>
<Col
className="text-gray"
data-testid={`${fieldName}-label`}
span={10}>
{fieldName}
</Col>
<Col data-testid={`${fieldName}-value`} span={12}>
{basicTableInfo[fieldName as keyof BasicTableInfo]}
</Col>
</Row>
</Col>
))}
</Row>
</Col>
</Row>
<Divider className="m-0" />
<SummaryPanelSkeleton loading={isLoading || isEmpty(tableDetails)}>
<>
<Row className="m-md" gutter={[0, 4]}>
<Col span={24}>
<Row>
{entityInfo.map((info) => {
const isOwner = info.name === t('label.owner');
<Row className={classNames('m-md')} gutter={[0, 16]}>
<Col span={24}>
<Typography.Text
className="section-header"
data-testid="profiler-header">
{t('label.profiler-amp-data-quality')}
</Typography.Text>
</Col>
<Col span={24}>
{isUndefined(overallSummary) ? (
<Typography.Text data-testid="no-profiler-enabled-message">
{t('message.no-profiler-enabled-summary-message')}
</Typography.Text>
) : (
<Row gutter={[16, 16]}>
{overallSummary.map((field) => (
<Col key={field.title} span={10}>
<Row>
<Col span={24}>
<Typography.Text
className="text-gray"
data-testid={`${field.title}-label`}>
{field.title}
</Typography.Text>
</Col>
<Col span={24}>
<Typography.Text
className={classNames(
'summary-panel-statistics-count',
field.className
return info.visible?.includes(componentType) ? (
<Col key={info.name} span={24}>
<Row
className={classNames('', {
'p-b-md': isOwner,
})}
gutter={[16, 32]}>
{!isOwner ? (
<Col data-testid={`${info.name}-label`} span={8}>
<Typography.Text className="text-grey-muted">
{info.name}
</Typography.Text>
</Col>
) : null}
<Col data-testid={`${info.name}-value`} span={16}>
{info.isLink ? (
<Link
component={Typography.Link}
target={info.isExternal ? '_blank' : '_self'}
to={{ pathname: info.url }}>
{info.value}
{info.isExternal ? (
<SVGIcons
alt="external-link"
className="m-l-xs"
icon="external-link"
width="12px"
/>
) : null}
</Link>
) : (
<Typography.Text
className={classNames('text-grey-muted', {
'text-grey-body': !isOwner,
})}>
{info.value}
</Typography.Text>
)}
data-testid={`${field.title}-value`}>
{field.value}
</Typography.Text>
</Col>
</Row>
</Col>
))}
</Col>
</Row>
</Col>
) : null;
})}
</Row>
)}
</Col>
</Row>
<Divider className="m-0" />
<Row className={classNames('m-md')} gutter={[0, 16]}>
<Col span={24}>
<Typography.Text
className="section-header"
data-testid="schema-header">
{t('label.schema')}
</Typography.Text>
</Col>
<Col span={24}>
<SummaryList
entityType={SummaryEntityType.COLUMN}
formattedEntityData={formattedColumnsData}
/>
</Col>
</Row>
</>
</Col>
</Row>
<Divider className="m-y-xs" />
{!isExplore ? (
<>
<SummaryTagsDescription
entityDetail={entityDetails}
tags={tags ? tags : []}
/>
<Divider className="m-y-xs" />
</>
) : null}
<Row className="m-md" gutter={[0, 16]}>
<Col span={24}>
<Typography.Text
className="text-base text-grey-muted"
data-testid="profiler-header">
{t('label.profiler-amp-data-quality')}
</Typography.Text>
</Col>
<Col span={24}>
{isUndefined(overallSummary) ? (
<Typography.Text
className="text-grey-body"
data-testid="no-profiler-enabled-message">
{t('message.no-profiler-enabled-summary-message')}
</Typography.Text>
) : (
<Row gutter={[0, 16]}>
{overallSummary.map((field) => (
<Col key={field.title} span={10}>
<Row>
<Col span={24}>
<Typography.Text
className="text-grey-muted"
data-testid={`${field.title}-label`}>
{field.title}
</Typography.Text>
</Col>
<Col span={24}>
<Typography.Text
className={classNames(
'summary-panel-statistics-count',
field.className
)}
data-testid={`${field.title}-value`}>
{field.value}
</Typography.Text>
</Col>
</Row>
</Col>
))}
</Row>
)}
</Col>
</Row>
<Divider className="m-y-xs" />
<Row className="m-md" gutter={[0, 16]}>
<Col span={24}>
<Typography.Text
className="text-base text-grey-muted"
data-testid="schema-header">
{t('label.schema')}
</Typography.Text>
</Col>
<Col span={24}>
<SummaryList
entityType={SummaryEntityType.COLUMN}
formattedEntityData={formattedColumnsData}
/>
</Col>
</Row>
</>
</SummaryPanelSkeleton>
);
}

View File

@ -11,10 +11,17 @@
* limitations under the License.
*/
import { Table, TableType } from '../../../../generated/entity/data/table';
import {
Table,
TableType,
TagLabel,
} from '../../../../generated/entity/data/table';
export interface TableSummaryProps {
entityDetails: Table;
componentType?: string;
tags?: TagLabel[];
isLoading?: boolean;
}
export interface BasicTableInfo {

View File

@ -13,7 +13,9 @@
import { act, render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { getLatestTableProfileByFqn } from 'rest/tableAPI';
import { DRAWER_NAVIGATION_OPTIONS } from 'utils/EntityUtils';
import { mockTableEntityDetails } from '../mocks/TableSummary.mock';
import TableSummary from './TableSummary.component';
@ -30,16 +32,6 @@ jest.mock('rest/tableAPI', () => ({
.mockImplementation(() => mockTableEntityDetails),
}));
jest.mock(
'../../../common/table-data-card-v2/TableDataCardTitle.component',
() =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="TableDataCardTitle">TableDataCardTitle</div>
))
);
jest.mock('../SummaryList/SummaryList.component', () =>
jest
.fn()
@ -47,33 +39,93 @@ jest.mock('../SummaryList/SummaryList.component', () =>
);
describe('TableSummary component tests', () => {
it('Component should render properly', async () => {
it('Component should render properly, when loaded in the Explore page.', async () => {
await act(async () => {
render(<TableSummary entityDetails={mockTableEntityDetails} />);
});
const tableTitle = screen.getByTestId('TableDataCardTitle');
const profilerHeader = screen.getByTestId('profiler-header');
const schemaHeader = screen.getByTestId('schema-header');
const typeLabel = screen.getByTestId('Type-label');
const queriesLabel = screen.getByTestId('Queries-label');
const columnsLabel = screen.getByTestId('Columns-label');
const typeValue = screen.getByTestId('Type-value');
const queriesValue = screen.getByTestId('Queries-value');
const columnsValue = screen.getByTestId('Columns-value');
const typeLabel = screen.getByTestId('label.type-label');
const queriesLabel = screen.getByTestId('label.query-plural-label');
const columnsLabel = screen.getByTestId('label.column-plural-label');
const typeValue = screen.getByTestId('label.type-value');
const queriesValue = screen.getByTestId('label.query-plural-value');
const columnsValue = screen.getByTestId('label.column-plural-value');
const noProfilerPlaceholder = screen.getByTestId(
'no-profiler-enabled-message'
);
const summaryList = screen.getByTestId('SummaryList');
expect(tableTitle).toBeInTheDocument();
expect(profilerHeader).toBeInTheDocument();
expect(schemaHeader).toBeInTheDocument();
expect(typeLabel).toBeInTheDocument();
expect(queriesLabel).toBeInTheDocument();
expect(columnsLabel).toBeInTheDocument();
expect(typeValue).toContainHTML('Regular');
expect(queriesValue).toContainHTML('2');
expect(queriesValue.textContent).toBe('2 past week');
expect(columnsValue).toContainHTML('2');
expect(noProfilerPlaceholder).toContainHTML(
'message.no-profiler-enabled-summary-message'
);
expect(summaryList).toBeInTheDocument();
});
it('Component should render properly, when loaded in the Lineage page.', async () => {
const labels = [
'label.service-label',
'label.type-label',
'label.database-label',
'label.schema-label',
'label.query-plural-label',
'label.column-plural-label',
];
const values = [
'label.type-value',
'label.service-value',
'label.database-value',
'label.schema-value',
];
render(
<TableSummary
componentType={DRAWER_NAVIGATION_OPTIONS.lineage}
entityDetails={mockTableEntityDetails}
/>,
{
wrapper: MemoryRouter,
}
);
const profilerHeader = screen.getByTestId('profiler-header');
const schemaHeader = screen.getAllByTestId('schema-header');
const queriesLabel = screen.getByTestId('label.query-plural-label');
const columnsLabel = screen.getByTestId('label.column-plural-label');
const typeValue = screen.getByTestId('label.type-value');
const queriesValue = screen.getByTestId('label.query-plural-value');
const columnsValue = screen.getByTestId('label.column-plural-value');
const noProfilerPlaceholder = screen.getByTestId(
'no-profiler-enabled-message'
);
const ownerLabel = screen.queryByTestId('label.owner-label');
const summaryList = screen.getByTestId('SummaryList');
expect(ownerLabel).not.toBeInTheDocument();
labels.forEach((label) =>
expect(screen.getByTestId(label)).toBeInTheDocument()
);
values.forEach((value) =>
expect(screen.getByTestId(value)).toBeInTheDocument()
);
expect(profilerHeader).toBeInTheDocument();
expect(schemaHeader[0]).toBeInTheDocument();
expect(queriesLabel).toBeInTheDocument();
expect(columnsLabel).toBeInTheDocument();
expect(typeValue).toContainHTML('Regular');
expect(queriesValue.textContent).toBe('2 past week');
expect(columnsValue).toContainHTML('2');
expect(noProfilerPlaceholder).toContainHTML(
'message.no-profiler-enabled-summary-message'

View File

@ -12,55 +12,80 @@
*/
import { Col, Divider, Row, Typography } from 'antd';
import SummaryTagsDescription from 'components/common/SummaryTagsDescription/SummaryTagsDescription.component';
import SummaryPanelSkeleton from 'components/Skeleton/SummaryPanelSkeleton/SummaryPanelSkeleton.component';
import { getTeamAndUserDetailsPath } from 'constants/constants';
import { isArray, isEmpty } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { getTopicByFqn } from 'rest/topicsAPI';
import {
DRAWER_NAVIGATION_OPTIONS,
getOwnerNameWithProfilePic,
} from 'utils/EntityUtils';
import { showErrorToast } from 'utils/ToastUtils';
import { SummaryEntityType } from '../../../../enums/EntitySummary.enum';
import { SearchIndex } from '../../../../enums/search.enum';
import { Topic } from '../../../../generated/entity/data/topic';
import { TagLabel, Topic } from '../../../../generated/entity/data/topic';
import { getFormattedEntityData } from '../../../../utils/EntitySummaryPanelUtils';
import { bytesToSize } from '../../../../utils/StringsUtils';
import { showErrorToast } from '../../../../utils/ToastUtils';
import { getConfigObject } from '../../../../utils/TopicDetailsUtils';
import TableDataCardTitle from '../../../common/table-data-card-v2/TableDataCardTitle.component';
import { TopicConfigObjectInterface } from '../../../TopicDetails/TopicDetails.interface';
import SummaryList from '../SummaryList/SummaryList.component';
import { BasicEntityInfo } from '../SummaryList/SummaryList.interface';
interface TopicSummaryProps {
entityDetails: Topic;
componentType?: string;
tags?: TagLabel[];
isLoading?: boolean;
}
function TopicSummary({ entityDetails }: TopicSummaryProps) {
function TopicSummary({
entityDetails,
componentType = DRAWER_NAVIGATION_OPTIONS.explore,
tags,
isLoading,
}: TopicSummaryProps) {
const { t } = useTranslation();
const [topicDetails, setTopicDetails] = useState<Topic>(entityDetails);
const isExplore = useMemo(
() => componentType === DRAWER_NAVIGATION_OPTIONS.explore,
[componentType]
);
const topicConfig = useMemo(() => {
const configs = getConfigObject(topicDetails);
const combined = { ...topicDetails, ...entityDetails };
const configs = getConfigObject(combined);
return {
...configs,
'Retention Size': bytesToSize(configs['Retention Size'] ?? 0),
'Max Message Size': bytesToSize(configs['Max Message Size'] ?? 0),
};
}, [topicDetails]);
}, [entityDetails, topicDetails]);
const formattedSchemaFieldsData: BasicEntityInfo[] = useMemo(
() =>
getFormattedEntityData(
SummaryEntityType.SCHEMAFIELD,
topicDetails.messageSchema?.schemaFields
),
[topicDetails]
);
const ownerDetails = useMemo(() => {
const owner = entityDetails.owner;
const fetchExtraTopicInfo = async () => {
return {
value:
getOwnerNameWithProfilePic(owner) ||
t('label.no-entity', {
entity: t('label.owner'),
}),
url: getTeamAndUserDetailsPath(owner?.name || ''),
isLink: owner?.name ? true : false,
};
}, [entityDetails, topicDetails]);
const fetchExtraTopicInfo = useCallback(async () => {
try {
const res = await getTopicByFqn(
entityDetails.fullyQualifiedName ?? '',
''
);
const res = await getTopicByFqn(entityDetails.fullyQualifiedName ?? '', [
'tags',
'owner',
]);
const { partitions, messageSchema } = res;
@ -73,73 +98,104 @@ function TopicSummary({ entityDetails }: TopicSummaryProps) {
})
);
}
};
useEffect(() => {
fetchExtraTopicInfo();
}, [entityDetails]);
const formattedSchemaFieldsData: BasicEntityInfo[] = useMemo(
() =>
getFormattedEntityData(
SummaryEntityType.SCHEMAFIELD,
topicDetails.messageSchema?.schemaFields
),
[topicDetails]
);
useEffect(() => {
if (entityDetails.service?.type === 'messagingService') {
fetchExtraTopicInfo();
}
}, [entityDetails, componentType]);
return (
<>
<Row className="m-md" gutter={[0, 4]}>
<Col span={24}>
<TableDataCardTitle
dataTestId="summary-panel-title"
searchIndex={SearchIndex.TOPIC}
source={entityDetails}
/>
</Col>
<Col span={24}>
<Row>
{Object.keys(topicConfig).map((fieldName) => {
const value =
topicConfig[fieldName as keyof TopicConfigObjectInterface];
<SummaryPanelSkeleton loading={Boolean(isLoading)}>
<>
<Row className="m-md" gutter={[0, 4]}>
{!isExplore ? (
<Col className="p-b-md" span={24}>
{ownerDetails.isLink ? (
<Link
component={Typography.Link}
to={{ pathname: ownerDetails.url }}>
{ownerDetails.value}
</Link>
) : (
<Typography.Text className="text-grey-muted">
{ownerDetails.value}
</Typography.Text>
)}
</Col>
) : null}
<Col span={24}>
<Row>
{Object.keys(topicConfig).map((fieldName) => {
const value =
topicConfig[fieldName as keyof TopicConfigObjectInterface];
const fieldValue = isArray(value) ? value.join(', ') : value;
const fieldValue = isArray(value) ? value.join(', ') : value;
return (
<Col key={fieldName} span={24}>
<Row gutter={16}>
<Col
className="text-gray"
data-testid={`${fieldName}-label`}
span={10}>
{fieldName}
</Col>
<Col data-testid={`${fieldName}-value`} span={12}>
{fieldValue ? fieldValue : '-'}
</Col>
</Row>
</Col>
);
})}
</Row>
</Col>
</Row>
<Divider className="m-0" />
<Row className="m-md" gutter={[0, 16]}>
<Col span={24}>
<Typography.Text
className="section-header"
data-testid="schema-header">
{t('label.schema')}
</Typography.Text>
</Col>
<Col span={24}>
{isEmpty(topicDetails.messageSchema?.schemaFields) ? (
<div className="m-y-md">
<Typography.Text
className="text-gray"
data-testid="no-data-message">
{t('message.no-data-available')}
</Typography.Text>
</div>
) : (
<SummaryList formattedEntityData={formattedSchemaFieldsData} />
)}
</Col>
</Row>
</>
return (
<Col key={fieldName} span={24}>
<Row gutter={[16, 32]}>
<Col data-testid={`${fieldName}-label`} span={10}>
<Typography.Text className="text-grey-muted">
{fieldName}
</Typography.Text>
</Col>
<Col data-testid={`${fieldName}-value`} span={14}>
{fieldValue ? fieldValue : '-'}
</Col>
</Row>
</Col>
);
})}
</Row>
</Col>
</Row>
<Divider className="m-y-xs" />
{!isExplore ? (
<>
<SummaryTagsDescription
entityDetail={entityDetails}
tags={tags ? tags : []}
/>
<Divider className="m-y-xs" />
</>
) : null}
<Row className="m-md" gutter={[0, 16]}>
<Col span={24}>
<Typography.Text
className="text-base text-grey-muted"
data-testid="schema-header">
{t('label.schema')}
</Typography.Text>
</Col>
<Col span={24}>
{isEmpty(topicDetails?.messageSchema?.schemaFields) ? (
<div className="m-y-md">
<Typography.Text data-testid="no-data-message">
<Typography.Text className="text-grey-body">
{t('message.no-data-available')}
</Typography.Text>
</Typography.Text>
</div>
) : (
<SummaryList formattedEntityData={formattedSchemaFieldsData} />
)}
</Col>
</Row>
</>
</SummaryPanelSkeleton>
);
}

View File

@ -20,16 +20,6 @@ import {
} from '../mocks/TopicSummary.mock';
import TopicSummary from './TopicSummary.component';
jest.mock(
'../../../common/table-data-card-v2/TableDataCardTitle.component',
() =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="TableDataCardTitle">TableDataCardTitle</div>
))
);
jest.mock('../SummaryList/SummaryList.component', () =>
jest
.fn()
@ -46,7 +36,6 @@ describe('TopicSummary component tests', () => {
render(<TopicSummary entityDetails={mockTopicEntityDetails} />);
});
const topicTitle = screen.getByTestId('TableDataCardTitle');
const partitionsLabel = screen.getByTestId('Partitions-label');
const replicationFactorLabel = screen.getByTestId(
'Replication Factor-label'
@ -64,13 +53,12 @@ describe('TopicSummary component tests', () => {
const schemaHeader = screen.getByTestId('schema-header');
const summaryList = screen.getByTestId('SummaryList');
expect(topicTitle).toBeInTheDocument();
expect(partitionsLabel).toBeInTheDocument();
expect(replicationFactorLabel).toBeInTheDocument();
expect(retentionSizeLabel).toBeInTheDocument();
expect(cleanUpPoliciesLabel).toBeInTheDocument();
expect(maxMessageSizeLabel).toBeInTheDocument();
expect(partitionsValue).toContainHTML('128');
expect(partitionsValue).toContainHTML('-');
expect(replicationFactorValue).toContainHTML('4');
expect(retentionSizeValue).toContainHTML('1018.83 MB');
expect(cleanUpPoliciesValue).toContainHTML('delete');

View File

@ -97,5 +97,48 @@ export const mockTableEntityDetails: Table = {
queryDate: mockDate,
},
],
service: {
id: '0875717c-5855-427c-8dd6-92d4cbfe7c51',
type: 'databaseService',
name: 'sample_data',
fullyQualifiedName: 'sample_data',
deleted: false,
href: 'http://localhost:8585/api/v1/services/databaseServices/0875717c-5855-427c-8dd6-92d4cbfe7c51',
},
usageSummary: {
dailyStats: {
count: 0,
percentileRank: 0,
},
weeklyStats: {
count: 2,
percentileRank: 0,
},
monthlyStats: {
count: 0,
percentileRank: 0,
},
date: '2023-02-01' as unknown as Date,
},
databaseSchema: {
id: '406d4782-b480-42a4-ab8b-e6fed20f3eef',
type: 'databaseSchema',
name: 'shopify',
fullyQualifiedName: 'sample_data.ecommerce_db.shopify',
description:
'This **mock** database contains schema related to shopify sales and orders with related dimension tables.',
deleted: false,
href: 'http://localhost:8585/api/v1/databaseSchemas/406d4782-b480-42a4-ab8b-e6fed20f3eef',
},
database: {
id: '78a58be0-26a9-4ac8-b515-067db85bbb41',
type: 'database',
name: 'ecommerce_db',
fullyQualifiedName: 'sample_data.ecommerce_db',
description:
'This **mock** database contains schemas related to shopify sales and orders with related dimension tables.',
deleted: false,
href: 'http://localhost:8585/api/v1/databases/78a58be0-26a9-4ac8-b515-067db85bbb41',
},
followers: [],
};

View File

@ -47,6 +47,14 @@ jest.mock('components/searched-data/SearchedData', () => {
));
});
jest.mock('./EntitySummaryPanel/EntitySummaryPanel.component', () =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="EntitySummaryPanel">EntitySummaryPanel</div>
))
);
const mockFunction = jest.fn();
jest.mock('../containers/PageLayoutV1', () =>

View File

@ -22,12 +22,14 @@ const LabelCountSkeleton = ({
labelProps,
selectProps,
countProps,
firstColSize = 20,
secondColSize = 4,
...props
}: LabelCountSkeletonProps) => {
return (
<Row justify="space-between" key={key}>
{isSelect || isLabel ? (
<Col span={20}>
<Col span={firstColSize}>
<div className="w-48 flex">
{isSelect ? (
<div>
@ -58,7 +60,7 @@ const LabelCountSkeleton = ({
</div>
</Col>
) : null}
<Col span={4}>
<Col span={secondColSize}>
{isCount ? (
<Skeleton
active

View File

@ -40,6 +40,8 @@ export interface LabelCountSkeletonProps extends SkeletonProps, Partial<Key> {
labelProps?: SkeletonProps;
selectProps?: SkeletonProps;
countProps?: SkeletonProps;
firstColSize?: number;
secondColSize?: number;
}
export type EntityListSkeletonProps = SkeletonInterface &

View File

@ -0,0 +1,52 @@
/*
* 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 ButtonSkeleton from 'components/Skeleton/CommonSkeletons/ControlElements/ControlElements.component';
import LabelCountSkeleton from 'components/Skeleton/CommonSkeletons/LabelCountSkeleton/LabelCountSkeleton.component';
import { SkeletonInterface } from 'components/Skeleton/Skeleton.interfaces';
import { getSkeletonMockData } from 'components/Skeleton/SkeletonUtils/Skeleton.utils';
import { uniqueId } from 'lodash';
import React from 'react';
const SummaryPanelSkeleton = ({ loading, children }: SkeletonInterface) => {
return loading ? (
<div className="m-b-md p-md">
<Row gutter={32} justify="space-between">
<Col className="m-t-md" span={24}>
{getSkeletonMockData(5).map(() => (
<LabelCountSkeleton
isCount
isLabel
firstColSize={8}
key={uniqueId()}
secondColSize={16}
title={{
width: 100,
}}
/>
))}
</Col>
<Col className="m-l-xss" span={24}>
{getSkeletonMockData(10).map(() => (
<ButtonSkeleton key={uniqueId()} size="large" />
))}
</Col>
</Row>
</div>
) : (
children
);
};
export default SummaryPanelSkeleton;

View File

@ -98,6 +98,7 @@ export interface TopicDetailsProps {
}
export interface TopicConfigObjectInterface {
Owner?: Record<string, string | JSX.Element | undefined>;
Partitions: number;
'Replication Factor'?: number;
'Retention Size'?: number;

View File

@ -0,0 +1,81 @@
/*
* 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, Divider, Row, Typography } from 'antd';
import TagsViewer from 'components/Tag/TagsViewer/tags-viewer';
import { TagLabel } from 'generated/type/tagLabel';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as TagIcon } from '../../../assets/svg/tag-grey.svg';
import { EntityData } from '../PopOverCard/EntityPopOverCard';
import RichTextEditorPreviewer from '../rich-text-editor/RichTextEditorPreviewer';
const SummaryTagsDescription = ({
tags,
entityDetail,
}: {
tags: TagLabel[];
entityDetail: EntityData;
}) => {
const { t } = useTranslation();
return (
<>
<Row className="m-md" gutter={[0, 16]}>
<Col span={24}>
<Typography.Text className="text-base text-grey-muted">
{t('label.tag-plural')}
</Typography.Text>
</Col>
<Col span={24}>
<div className="flex flex-wrap items-center">
{tags.length > 0 ? (
<>
<TagIcon className="m-r-xs" data-testid="tag-grey-icon" />
<TagsViewer sizeCap={-1} tags={tags} />
</>
) : (
<Typography.Text className="text-grey-body">
{t('label.no-tags-added')}
</Typography.Text>
)}
</div>
</Col>
</Row>
<Divider className="m-y-xs" />
<Row className="m-md" gutter={[0, 16]}>
<Col span={24}>
<Typography.Text
className="text-base text-grey-muted"
data-testid="schema-header">
{t('label.description')}
</Typography.Text>
</Col>
<Col span={24}>
<div>
{entityDetail.description?.trim() ? (
<RichTextEditorPreviewer markdown={entityDetail.description} />
) : (
<Typography className="text-grey-body">
{t('label.no-data-found')}
</Typography>
)}
</div>
</Col>
</Row>
</>
);
};
export default SummaryTagsDescription;

View File

@ -12,6 +12,7 @@
*/
import { Button, Typography } from 'antd';
import classNames from 'classnames';
import { toString } from 'lodash';
import React, { useMemo } from 'react';
import { Link } from 'react-router-dom';
@ -32,6 +33,7 @@ interface TableDataCardTitleProps {
id?: string;
searchIndex: SearchIndex | EntityType;
source: SourceType;
isPanel?: boolean;
handleLinkClick?: (e: React.MouseEvent) => void;
}
@ -41,6 +43,7 @@ const TableDataCardTitle = ({
searchIndex,
source,
handleLinkClick,
isPanel = false,
}: TableDataCardTitleProps) => {
const isTourRoute = location.pathname.includes(ROUTES.TOUR);
@ -76,7 +79,12 @@ const TableDataCardTitle = ({
level={5}
title={displayName}>
<Link
className="table-data-card-title-container w-fit-content w-max-90"
className={classNames(
'table-data-card-title-container w-fit-content w-max-90',
{
'button-hover': isPanel,
}
)}
to={getEntityLink(searchIndex, source.fullyQualifiedName ?? '')}>
{title}
</Link>

View File

@ -10,7 +10,7 @@
* 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 {
@ -24,3 +24,11 @@
color: @link-btn-color;
}
}
.button-hover {
.ant-btn-link > span {
&:hover {
color: @primary-color;
}
}
}

View File

@ -1161,6 +1161,7 @@
"entity-fetch-error": "Error while fetching {{entity}}",
"entity-fetch-version-error": "Error while fetching {{entity}} versions {{version}}",
"entity-updating-error": "Error while updating {{entity}}",
"error-selected-node-name-details": "Error while getting {{selectedNodeName}} details",
"error-while-renewing-id-token-with-message": "Error while renewing id token from Auth0 SSO: {{message}}",
"feed-post-error": "Error while posting the message!",
"fetch-entity-permissions-error": "Unable to get permission for {{entity}}.",

View File

@ -41,7 +41,7 @@ export const getMlModelVersion = async (id: string, version: string) => {
export const getMlModelByFQN = async (
fqn: string,
arrQueryFields: string,
arrQueryFields: string | string[],
include = Include.All
) => {
const url = getURLWithQueryFields(

View File

@ -67,7 +67,7 @@ export const getTopicDetails = (
export const getTopicByFqn = async (
fqn: string,
arrQueryFields: string | TabSpecificField[]
arrQueryFields: string[] | string | TabSpecificField[]
) => {
const url = getURLWithQueryFields(
`/topics/name/${fqn}`,

View File

@ -49,30 +49,6 @@
font-weight: 600;
}
// font size
.text-xs {
font-size: 12px;
}
.text-lg {
font-size: 18px;
}
.text-sm {
font-size: 14px;
}
.text-base {
font-size: 1rem /* 16px */;
line-height: 1.5rem /* 24px */;
}
.text-xl {
font-size: 1.25rem /* 20px */;
line-height: 1.75rem /* 28px */;
}
.text-2xl {
font-size: 1.5rem /* 24px */;
line-height: 2rem /* 32px */;
}
// text color
.text-primary {
color: @primary;

View File

@ -17,6 +17,8 @@
@success-color: #008376;
@warning-color: #ffc34e;
@error-color: #ff4c3b;
@failed-color: #cb2431;
@aborted-color: #efae2f;
@info-color: #1890ff;
@text-color: #000000;
@text-color-secondary: #37352f;

View File

@ -12,6 +12,7 @@
*/
import { CheckOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
import { Button, Typography } from 'antd';
import {
CustomEdgeData,
CustomElement,
@ -91,13 +92,20 @@ export const getHeaderLabel = (
{name || prepareLabel(type, fqn, false)}
</span>
) : (
<span
className="tw-break-words description-text tw-self-center link-text tw-font-medium"
data-testid="lineage-entity">
<Link to={getEntityLink(type, fqn)}>
{name || prepareLabel(type, fqn, false)}
<Typography.Title
ellipsis
className="m-b-0 text-base"
level={5}
title={name || prepareLabel(type, fqn, false)}>
<Link className="" to={getEntityLink(type, fqn)}>
<Button
className="text-base font-semibold p-0"
data-testid="link-button"
type="link">
{name || prepareLabel(type, fqn, false)}
</Button>
</Link>
</span>
</Typography.Title>
)}
</Fragment>
);

View File

@ -57,7 +57,9 @@ export const getFormattedEntityData = (
title: (
<Link target="_blank" to={{ pathname: chart.chartUrl }}>
<Space className="m-b-xs">
<Text className="entity-title link">{getEntityName(chart)}</Text>
<Text className="entity-title text-primary font-medium">
{getEntityName(chart)}
</Text>
<SVGIcons alt="external-link" icon="external-link" width="12px" />
</Space>
</Link>
@ -73,7 +75,9 @@ export const getFormattedEntityData = (
title: (
<Link target="_blank" to={{ pathname: task.taskUrl }}>
<Space className="m-b-xs">
<Text className="entity-title link">{task.name}</Text>
<Text className="entity-title text-primary font-medium">
{task.name}
</Text>
<SVGIcons alt="external-link" icon="external-link" width="12px" />
</Space>
</Link>

View File

@ -13,11 +13,14 @@
import { Popover } from 'antd';
import { EntityData } from 'components/common/PopOverCard/EntityPopOverCard';
import ProfilePicture from 'components/common/ProfilePicture/ProfilePicture';
import {
LeafNodes,
LineagePos,
} from 'components/EntityLineage/EntityLineage.interface';
import { ResourceEntity } from 'components/PermissionProvider/PermissionProvider.interface';
import { ExplorePageTabs } from 'enums/Explore.enum';
import { Mlmodel } from 'generated/entity/data/mlmodel';
import i18next from 'i18next';
import { isEmpty, isNil, isUndefined, lowerCase, startCase } from 'lodash';
import { Bucket } from 'Models';
@ -25,11 +28,11 @@ import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
import {
getDashboardDetailsPath,
getDatabaseDetailsPath,
getDatabaseSchemaDetailsPath,
getServiceDetailsPath,
getTableDetailsPath,
getTeamAndUserDetailsPath,
} from '../constants/constants';
import { AssetsType, EntityType, FqnPart } from '../enums/entity.enum';
import { SearchIndex } from '../enums/search.enum';
@ -42,6 +45,7 @@ import {
ColumnJoins,
JoinedWith,
Table,
TableType,
} from '../generated/entity/data/table';
import { Topic } from '../generated/entity/data/topic';
import { Edge, EntityLineage } from '../generated/type/entityLineage';
@ -49,6 +53,7 @@ import { EntityReference } from '../generated/type/entityUsage';
import { TagLabel } from '../generated/type/tagLabel';
import {
getEntityName,
getOwnerValue,
getPartialNameFromTableFQN,
getTableFQNFromColumnFQN,
} from './CommonUtils';
@ -59,10 +64,15 @@ import {
} from './TableUtils';
import { getTableTags } from './TagsUtils';
export enum DRAWER_NAVIGATION_OPTIONS {
explore = 'Explore',
lineage = 'Lineage',
}
export const getEntityTags = (
type: string,
entityDetail: Table | Pipeline | Dashboard | Topic
): Array<TagLabel | undefined> => {
entityDetail: Table | Pipeline | Dashboard | Topic | Mlmodel
): Array<TagLabel> => {
switch (type) {
case EntityType.TABLE: {
const tableTags: Array<TagLabel> = [
@ -72,10 +82,10 @@ export const getEntityTags = (
return tableTags;
}
case EntityType.PIPELINE: {
return entityDetail.tags || [];
}
case EntityType.DASHBOARD: {
case EntityType.PIPELINE:
case EntityType.DASHBOARD:
case EntityType.TOPIC:
case EntityType.MLMODEL: {
return entityDetail.tags || [];
}
@ -84,19 +94,38 @@ export const getEntityTags = (
}
};
export const getOwnerNameWithProfilePic = (
owner: EntityReference | undefined
) =>
owner ? (
<div className="flex items-center gap-2">
{' '}
<ProfilePicture
displayName={owner.displayName}
id={owner.id as string}
name={owner.name || ''}
width="20"
/>
<span>{getEntityName(owner)}</span>
</div>
) : null;
export const getEntityOverview = (
type: string,
entityDetail: EntityData,
serviceType: string
entityDetail: EntityData
): Array<{
name: string;
value: string | number | React.ReactNode;
isLink: boolean;
isExternal?: boolean;
url?: string;
visible?: Array<string>;
dataTestId?: string;
}> => {
const NO_DATA = '-';
switch (type) {
case EntityType.TABLE: {
case ExplorePageTabs.TABLES: {
const {
fullyQualifiedName,
owner,
@ -104,31 +133,56 @@ export const getEntityOverview = (
usageSummary,
profile,
columns,
tableType,
} = entityDetail as Table;
const [service, database, schema] = getPartialNameFromTableFQN(
fullyQualifiedName ?? '',
[FqnPart.Service, FqnPart.Database, FqnPart.Schema],
FQN_SEPARATOR_CHAR
).split(FQN_SEPARATOR_CHAR);
const tier = getTierFromTableTags(tags || []);
const usage = !isNil(usageSummary?.weeklyStats?.percentileRank)
? getUsagePercentile(usageSummary?.weeklyStats?.percentileRank || 0)
: '--';
const queries = usageSummary?.weeklyStats?.count.toLocaleString() || '--';
: '-';
const queries = usageSummary?.weeklyStats?.count.toLocaleString() || '0';
const overview = [
{
name: i18next.t('label.owner'),
value:
getOwnerNameWithProfilePic(owner) ||
i18next.t('label.no-entity', {
entity: i18next.t('label.owner'),
}),
url: getOwnerValue(owner as EntityReference),
isLink: owner?.name ? true : false,
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
},
{
name: i18next.t('label.type'),
value: tableType || TableType.Regular,
isLink: false,
visible: [
DRAWER_NAVIGATION_OPTIONS.lineage,
DRAWER_NAVIGATION_OPTIONS.explore,
],
},
{
name: i18next.t('label.service'),
value: service,
value: service || NO_DATA,
url: getServiceDetailsPath(
service,
ServiceCategory.DATABASE_SERVICES
),
isLink: true,
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
},
{
name: i18next.t('label.database'),
value: database,
value: database || NO_DATA,
url: getDatabaseDetailsPath(
getPartialNameFromTableFQN(
fullyQualifiedName ?? '',
@ -137,10 +191,11 @@ export const getEntityOverview = (
)
),
isLink: true,
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
},
{
name: i18next.t('label.schema'),
value: schema,
value: schema || NO_DATA,
url: getDatabaseSchemaDetailsPath(
getPartialNameFromTableFQN(
fullyQualifiedName ?? '',
@ -149,126 +204,217 @@ export const getEntityOverview = (
)
),
isLink: true,
},
{
name: i18next.t('label.owner'),
value: getEntityName(owner) || '--',
url: getTeamAndUserDetailsPath(owner?.name || ''),
isLink: owner ? owner.type === 'team' : false,
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
},
{
name: i18next.t('label.tier'),
value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : '--',
value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA,
isLink: false,
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
},
{
name: i18next.t('label.usage'),
value: usage,
value: usage || NO_DATA,
isLink: false,
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
},
{
name: i18next.t('label.query-plural'),
value: `${queries} past week`,
isLink: false,
visible: [
DRAWER_NAVIGATION_OPTIONS.lineage,
DRAWER_NAVIGATION_OPTIONS.explore,
],
},
{
name: i18next.t('label.column-plural'),
value: columns ? columns.length : '--',
value: columns ? columns.length : NO_DATA,
isLink: false,
visible: [
DRAWER_NAVIGATION_OPTIONS.lineage,
DRAWER_NAVIGATION_OPTIONS.explore,
],
},
{
name: i18next.t('label.row-plural'),
value: profile && profile?.rowCount ? profile.rowCount : '--',
value: profile && profile?.rowCount ? profile.rowCount : NO_DATA,
isLink: false,
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
},
];
return overview;
}
case EntityType.PIPELINE: {
const { owner, tags, pipelineUrl, service, fullyQualifiedName } =
case ExplorePageTabs.PIPELINES: {
const { owner, tags, pipelineUrl, service, displayName } =
entityDetail as Pipeline;
const tier = getTierFromTableTags(tags || []);
const overview = [
{
name: i18next.t('label.owner'),
value:
getOwnerNameWithProfilePic(owner) ||
i18next.t('label.no-entity', {
entity: i18next.t('label.owner'),
}),
url: getOwnerValue(owner as EntityReference),
isLink: owner?.name ? true : false,
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
},
{
name: `${i18next.t('label.pipeline')} ${i18next.t(
'label.url-uppercase'
)}`,
dataTestId: 'pipeline-url-label',
value: displayName || NO_DATA,
url: pipelineUrl,
isLink: true,
isExternal: true,
visible: [
DRAWER_NAVIGATION_OPTIONS.lineage,
DRAWER_NAVIGATION_OPTIONS.explore,
],
},
{
name: i18next.t('label.service'),
value: service?.name as string,
value: (service?.name as string) || NO_DATA,
url: getServiceDetailsPath(
service?.name as string,
ServiceCategory.PIPELINE_SERVICES
),
isLink: true,
},
{
name: i18next.t('label.owner'),
value: getEntityName(owner) || '--',
url: getTeamAndUserDetailsPath(owner?.name || ''),
isLink: owner ? owner.type === 'team' : false,
isExternal: false,
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
},
{
name: i18next.t('label.tier'),
value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : '--',
value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA,
isLink: false,
},
{
name: `${serviceType} ${i18next.t('label.url-lowercase')}`,
value: fullyQualifiedName?.split(FQN_SEPARATOR_CHAR)[1] as string,
url: pipelineUrl as string,
isLink: true,
isExternal: true,
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
},
];
return overview;
}
case EntityType.DASHBOARD: {
const {
owner,
tags,
dashboardUrl,
service,
fullyQualifiedName,
displayName,
} = entityDetail as Dashboard;
case ExplorePageTabs.DASHBOARDS: {
const { owner, tags, dashboardUrl, service, displayName } =
entityDetail as Dashboard;
const tier = getTierFromTableTags(tags || []);
const overview = [
{
name: i18next.t('label.owner'),
value:
getOwnerNameWithProfilePic(owner) ||
i18next.t('label.no-entity', {
entity: i18next.t('label.owner'),
}),
url: getOwnerValue(owner as EntityReference),
isLink: owner?.name ? true : false,
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
},
{
name: `${i18next.t('label.dashboard')} ${i18next.t(
'label.url-uppercase'
)}`,
value: displayName || NO_DATA,
url: dashboardUrl,
isLink: true,
isExternal: true,
visible: [
DRAWER_NAVIGATION_OPTIONS.lineage,
DRAWER_NAVIGATION_OPTIONS.explore,
],
},
{
name: i18next.t('label.service'),
value: service?.name as string,
value: (service?.fullyQualifiedName as string) || NO_DATA,
url: getServiceDetailsPath(
service?.name as string,
ServiceCategory.DASHBOARD_SERVICES
),
isExternal: false,
isLink: true,
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
},
{
name: i18next.t('label.owner'),
value: getEntityName(owner) || '--',
url: getTeamAndUserDetailsPath(owner?.name || ''),
isLink: owner ? owner.type === 'team' : false,
},
{
name: i18next.t('label.tier'),
value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : '--',
value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA,
isLink: false,
},
{
name: `${serviceType} ${i18next.t('label.url-lowercase')}`,
value:
displayName ||
(fullyQualifiedName?.split(FQN_SEPARATOR_CHAR)[1] as string),
url: dashboardUrl as string,
isLink: true,
isExternal: true,
isExternal: false,
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
},
];
return overview;
}
case ExplorePageTabs.MLMODELS: {
const { algorithm, target, server, dashboard, owner } =
entityDetail as Mlmodel;
const overview = [
{
name: i18next.t('label.owner'),
value:
getOwnerNameWithProfilePic(owner) ||
i18next.t('label.no-entity', {
entity: i18next.t('label.owner'),
}),
url: getOwnerValue(owner as EntityReference),
isLink: owner?.name ? true : false,
visible: [DRAWER_NAVIGATION_OPTIONS.lineage],
},
{
name: i18next.t('label.algorithm'),
value: algorithm || NO_DATA,
url: '',
isLink: false,
visible: [
DRAWER_NAVIGATION_OPTIONS.lineage,
DRAWER_NAVIGATION_OPTIONS.explore,
],
},
{
name: i18next.t('label.target'),
value: target || NO_DATA,
url: '',
isLink: false,
visible: [
DRAWER_NAVIGATION_OPTIONS.lineage,
DRAWER_NAVIGATION_OPTIONS.explore,
],
},
{
name: i18next.t('label.server'),
value: server || NO_DATA,
url: server,
isLink: true,
isExternal: true,
visible: [
DRAWER_NAVIGATION_OPTIONS.lineage,
DRAWER_NAVIGATION_OPTIONS.explore,
],
},
{
name: i18next.t('label.dashboard'),
value: getEntityName(dashboard) || NO_DATA,
url: getDashboardDetailsPath(dashboard?.fullyQualifiedName as string),
isLink: true,
isExternal: false,
visible: [
DRAWER_NAVIGATION_OPTIONS.lineage,
DRAWER_NAVIGATION_OPTIONS.explore,
],
},
];
return overview;
}
default:
return [];
}

View File

@ -90,7 +90,7 @@ export const getUsagePercentile = (pctRank: number, isLiteral = false) => {
const ordinalPercentile = ordinalize(percentile);
const usagePercentile = `${
isLiteral ? t('label.usage') : ''
} - ${ordinalPercentile} ${t('label.pctile-lowercase')}`;
} ${ordinalPercentile} ${t('label.pctile-lowercase')}`;
return usagePercentile;
};