UI: Improved summary panel (#9470)

* Improved topic summary panel content

* added functionality to show nested column and schema details inside the EntitySummaryPanel.

* added unit tests for SummaryListItems component

* added unit tests for summary panel components
code optimizations and improvements

* fixed localization in SummaryListItems component
fixed unit tests

* fixed codesmells

* - added schema type field to show for topic summary
- fixed width issue for nested field details in summary panel
- added unit tests for EntitySummaryPanelUtils
- moved imports on top in EntitySummaryPanel test

* fixed failing unit tests

* Fixed failing unit tests

Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
This commit is contained in:
Aniket Katkar 2023-01-12 18:05:08 +05:30 committed by GitHub
parent e52e4207f7
commit 91a794aaa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 2398 additions and 278 deletions

View File

@ -14,16 +14,19 @@
import { Col, Divider, Row, Space, Typography } from 'antd';
import { AxiosError } from 'axios';
import { ChartType } from 'pages/DashboardDetailsPage/DashboardDetailsPage.component';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
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;
@ -57,6 +60,11 @@ function DashboardSummary({ entityDetails }: DashboardSummaryProps) {
fetchChartsDetails();
}, [entityDetails]);
const formattedChartsData: BasicEntityInfo[] = useMemo(
() => getFormattedEntityData(SummaryEntityType.CHART, charts),
[charts]
);
return (
<>
<Row className="m-md" gutter={[0, 4]}>
@ -69,24 +77,33 @@ function DashboardSummary({ entityDetails }: DashboardSummaryProps) {
</Col>
<Col span={24}>
<Row gutter={16}>
<Col className="text-gray" span={10}>
<Col
className="text-gray"
data-testid="dashboard-url-label"
span={10}>
{`${t('label.dashboard')} ${t('label.url-uppercase')}`}
</Col>
<Col span={12}>
<Link
target="_blank"
to={{ pathname: entityDetails.dashboardUrl }}>
<Space align="start">
<Typography.Text className="link">
{entityDetails.name}
</Typography.Text>
<SVGIcons
alt="external-link"
icon="external-link"
width="12px"
/>
</Space>
</Link>
<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>
@ -94,12 +111,14 @@ function DashboardSummary({ entityDetails }: DashboardSummaryProps) {
<Divider className="m-0" />
<Row className="m-md" gutter={[0, 16]}>
<Col span={24}>
<Typography.Text className="section-header">
<Typography.Text
className="section-header"
data-testid="charts-header">
{t('label.chart-plural')}
</Typography.Text>
</Col>
<Col span={24}>
<SummaryList charts={charts || []} />
<SummaryList formattedEntityData={formattedChartsData} />
</Col>
</Row>
</>

View File

@ -0,0 +1,83 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { act, render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import {
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()
.mockImplementation(() => <div data-testid="SummaryList">SummaryList</div>)
);
jest.mock('../../../../utils/DashboardDetailsUtils', () => ({
fetchCharts: jest.fn().mockImplementation(() => mockFetchChartsResponse),
}));
describe('DashboardSummary component tests', () => {
it('Component should render properly', async () => {
await act(async () => {
render(<DashboardSummary entityDetails={mockDashboardEntityDetails} />, {
wrapper: MemoryRouter,
});
});
const dashboardTitle = screen.getByTestId('TableDataCardTitle');
const dashboardUrlLabel = screen.getByTestId('dashboard-url-label');
const dashboardUrlValue = screen.getByTestId('dashboard-link-name');
const chartsHeader = screen.getByTestId('charts-header');
const summaryList = screen.getByTestId('SummaryList');
expect(dashboardTitle).toBeInTheDocument();
expect(dashboardUrlLabel).toBeInTheDocument();
expect(dashboardUrlValue).toContainHTML(mockDashboardEntityDetails.name);
expect(chartsHeader).toBeInTheDocument();
expect(summaryList).toBeInTheDocument();
});
it('If the dashboard url is not present in dashboard details, "-" should be displayed as dashboard url value', async () => {
await act(async () => {
render(
<DashboardSummary
entityDetails={{
...mockDashboardEntityDetails,
dashboardUrl: undefined,
}}
/>,
{
wrapper: MemoryRouter,
}
);
const dashboardUrlValue = screen.getByTestId('dashboard-url-value');
expect(dashboardUrlValue).toContainHTML('-');
});
});
});

View File

@ -68,7 +68,11 @@ export default function EntitySummaryPanel({
return (
<div className={classNames('summary-panel-container')}>
{summaryComponent}
<CloseOutlined className="close-icon" onClick={handleClosePanel} />
<CloseOutlined
className="close-icon"
data-testid="summary-panel-close-icon"
onClick={handleClosePanel}
/>
</div>
);
}

View File

@ -0,0 +1,191 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { ExplorePageTabs } from '../../../enums/Explore.enum';
import EntitySummaryPanel from './EntitySummaryPanel.component';
import { mockDashboardEntityDetails } from './mocks/DashboardSummary.mock';
import { mockMlModelEntityDetails } from './mocks/MlModelSummary.mock';
import { mockPipelineEntityDetails } from './mocks/PipelineSummary.mock';
import { mockTableEntityDetails } from './mocks/TableSummary.mock';
import { mockTopicEntityDetails } from './mocks/TopicSummary.mock';
const mockHandleClosePanel = jest.fn();
jest.mock('./TableSummary/TableSummary.component', () =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="TableSummary">TableSummary</div>
))
);
jest.mock('./TopicSummary/TopicSummary.component', () =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="TopicSummary">TopicSummary</div>
))
);
jest.mock('./DashboardSummary/DashboardSummary.component', () =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="DashboardSummary">DashboardSummary</div>
))
);
jest.mock('./PipelineSummary/PipelineSummary.component', () =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="PipelineSummary">PipelineSummary</div>
))
);
jest.mock('./MlModelSummary/MlModelSummary.component', () =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="MlModelSummary">MlModelSummary</div>
))
);
jest.mock('react-router-dom', () => ({
useParams: jest.fn().mockImplementation(() => ({ tab: 'table' })),
}));
describe('EntitySummaryPanel component tests', () => {
it('TableSummary should render for table data', async () => {
render(
<EntitySummaryPanel
entityDetails={{
details: mockTableEntityDetails,
entityType: ExplorePageTabs.TABLES,
}}
handleClosePanel={mockHandleClosePanel}
/>
);
const tableSummary = screen.getByTestId('TableSummary');
const closeIcon = screen.getByTestId('summary-panel-close-icon');
expect(tableSummary).toBeInTheDocument();
expect(closeIcon).toBeInTheDocument();
await act(async () => {
userEvent.click(closeIcon);
});
expect(mockHandleClosePanel).toHaveBeenCalledTimes(1);
});
it('TopicSummary should render for topics data', async () => {
render(
<EntitySummaryPanel
entityDetails={{
details: mockTopicEntityDetails,
entityType: ExplorePageTabs.TOPICS,
}}
handleClosePanel={mockHandleClosePanel}
/>
);
const topicSummary = screen.getByTestId('TopicSummary');
const closeIcon = screen.getByTestId('summary-panel-close-icon');
expect(topicSummary).toBeInTheDocument();
expect(closeIcon).toBeInTheDocument();
await act(async () => {
userEvent.click(closeIcon);
});
expect(mockHandleClosePanel).toHaveBeenCalledTimes(1);
});
it('DashboardSummary should render for dashboard data', async () => {
render(
<EntitySummaryPanel
entityDetails={{
details: mockDashboardEntityDetails,
entityType: ExplorePageTabs.DASHBOARDS,
}}
handleClosePanel={mockHandleClosePanel}
/>
);
const dashboardSummary = screen.getByTestId('DashboardSummary');
const closeIcon = screen.getByTestId('summary-panel-close-icon');
expect(dashboardSummary).toBeInTheDocument();
expect(closeIcon).toBeInTheDocument();
await act(async () => {
userEvent.click(closeIcon);
});
expect(mockHandleClosePanel).toHaveBeenCalledTimes(1);
});
it('PipelineSummary should render for pipeline data', async () => {
render(
<EntitySummaryPanel
entityDetails={{
details: mockPipelineEntityDetails,
entityType: ExplorePageTabs.PIPELINES,
}}
handleClosePanel={mockHandleClosePanel}
/>
);
const pipelineSummary = screen.getByTestId('PipelineSummary');
const closeIcon = screen.getByTestId('summary-panel-close-icon');
expect(pipelineSummary).toBeInTheDocument();
expect(closeIcon).toBeInTheDocument();
await act(async () => {
userEvent.click(closeIcon);
});
expect(mockHandleClosePanel).toHaveBeenCalledTimes(1);
});
it('MlModelSummary should render for mlModel data', async () => {
render(
<EntitySummaryPanel
entityDetails={{
details: mockMlModelEntityDetails,
entityType: ExplorePageTabs.MLMODELS,
}}
handleClosePanel={mockHandleClosePanel}
/>
);
const mlModelSummary = screen.getByTestId('MlModelSummary');
const closeIcon = screen.getByTestId('summary-panel-close-icon');
expect(mlModelSummary).toBeInTheDocument();
expect(closeIcon).toBeInTheDocument();
await act(async () => {
userEvent.click(closeIcon);
});
expect(mockHandleClosePanel).toHaveBeenCalledTimes(1);
});
});

View File

@ -12,15 +12,19 @@
*/
import { Col, Divider, Row, Typography } from 'antd';
import { startCase } from 'lodash';
import React, { ReactNode, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { getDashboardDetailsPath } from '../../../../constants/constants';
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;
@ -55,6 +59,15 @@ function MlModelSummary({ entityDetails }: MlModelSummaryProps) {
[entityDetails]
);
const formattedFeaturesData: BasicEntityInfo[] = useMemo(
() =>
getFormattedEntityData(
SummaryEntityType.MLFEATURE,
entityDetails.mlFeatures
),
[entityDetails]
);
return (
<>
<Row className="m-md" gutter={[0, 4]}>
@ -70,22 +83,22 @@ function MlModelSummary({ entityDetails }: MlModelSummaryProps) {
{Object.keys(basicMlModelInfo).map((fieldName) => {
const value =
basicMlModelInfo[fieldName as keyof BasicMlModelInfo];
if (value) {
return (
<Col key={fieldName} span={24}>
<Row gutter={16}>
<Col className="text-gray" span={10}>
{fieldName}
</Col>
<Col span={12}>
{basicMlModelInfo[fieldName as keyof BasicMlModelInfo]}
</Col>
</Row>
</Col>
);
} else {
return null;
}
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>
@ -93,12 +106,14 @@ function MlModelSummary({ entityDetails }: MlModelSummaryProps) {
<Divider className="m-0" />
<Row className="m-md" gutter={[0, 16]}>
<Col span={24}>
<Typography.Text className="section-header">
<Typography.Text
className="section-header"
data-testid="features-header">
{t('label.feature-plural')}
</Typography.Text>
</Col>
<Col span={24}>
<SummaryList mlFeatures={entityDetails.mlFeatures || []} />
<SummaryList formattedEntityData={formattedFeaturesData} />
</Col>
</Row>
</>

View File

@ -0,0 +1,89 @@
/*
* 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 {
mockMlModelEntityDetails,
mockMlModelEntityDetails1,
} 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()
.mockImplementation(() => <div data-testid="SummaryList">SummaryList</div>)
);
describe('MlModelSummary component tests', () => {
it('Component should render properly', () => {
render(<MlModelSummary entityDetails={mockMlModelEntityDetails} />, {
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');
expect(mlModelTitle).toBeInTheDocument();
expect(algorithmLabel).toBeInTheDocument();
expect(targetLabel).toBeInTheDocument();
expect(serverLabel).toBeInTheDocument();
expect(dashboardLabel).toBeInTheDocument();
expect(algorithmValue).toContainHTML('Neural Network');
expect(targetValue).toContainHTML('ETA_time');
expect(serverValue).toContainHTML('http://my-server.ai');
expect(dashboardValue).toBeInTheDocument();
});
it('Fields with no data should display "-" in value', () => {
render(<MlModelSummary entityDetails={mockMlModelEntityDetails1} />, {
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');
expect(algorithmLabel).toBeInTheDocument();
expect(targetLabel).toBeInTheDocument();
expect(serverLabel).toBeInTheDocument();
expect(dashboardLabel).toBeInTheDocument();
expect(algorithmValue).toContainHTML('Time Series');
expect(targetValue).toContainHTML('-');
expect(serverValue).toContainHTML('-');
expect(dashboardValue).toContainHTML('-');
});
});

View File

@ -12,16 +12,17 @@
*/
import { Col, Divider, Row, Space, Typography } from 'antd';
import { AxiosError } from 'axios';
import React, { useEffect, useState } from 'react';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { SummaryEntityType } from '../../../../enums/EntitySummary.enum';
import { SearchIndex } from '../../../../enums/search.enum';
import { Pipeline, Task } from '../../../../generated/entity/data/pipeline';
import { Pipeline } from '../../../../generated/entity/data/pipeline';
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 PipelineSummaryProps {
entityDetails: Pipeline;
@ -29,28 +30,11 @@ interface PipelineSummaryProps {
function PipelineSummary({ entityDetails }: PipelineSummaryProps) {
const { t } = useTranslation();
const [tasks, setTasks] = useState<Task[]>();
const fetchTaskDetails = async () => {
try {
const updatedTasks = (entityDetails.tasks || []).map((task) => ({
...task,
taskUrl: task.taskUrl,
}));
setTasks(updatedTasks);
} catch (err) {
showErrorToast(
err as AxiosError,
t('server.entity-fetch-error', {
entity: t('label.pipeline-detail-plural-lowercase'),
})
);
}
};
useEffect(() => {
fetchTaskDetails();
}, [entityDetails]);
const formattedTasksData: BasicEntityInfo[] = useMemo(
() => getFormattedEntityData(SummaryEntityType.TASK, entityDetails.tasks),
[entityDetails]
);
return (
<>
@ -64,24 +48,33 @@ function PipelineSummary({ entityDetails }: PipelineSummaryProps) {
</Col>
<Col span={24}>
<Row gutter={16}>
<Col className="text-gray" span={10}>
<Col
className="text-gray"
data-testid="pipeline-url-label"
span={10}>
{`${t('label.pipeline')} ${t('label.url-uppercase')}`}
</Col>
<Col span={12}>
<Link
target="_blank"
to={{ pathname: entityDetails.pipelineUrl }}>
<Space align="start">
<Typography.Text className="link">
{entityDetails.name}
</Typography.Text>
<SVGIcons
alt="external-link"
icon="external-link"
width="12px"
/>
</Space>
</Link>
<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>
@ -89,12 +82,14 @@ function PipelineSummary({ entityDetails }: PipelineSummaryProps) {
<Divider className="m-0" />
<Row className="m-md" gutter={[0, 16]}>
<Col span={24}>
<Typography.Text className="section-header">
<Typography.Text
className="section-header"
data-testid="tasks-header">
{t('label.task-plural')}
</Typography.Text>
</Col>
<Col span={24}>
<SummaryList tasks={tasks || []} />
<SummaryList formattedEntityData={formattedTasksData} />
</Col>
</Row>
</>

View File

@ -0,0 +1,69 @@
/*
* 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 { 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()
.mockImplementation(() => <div data-testid="SummaryList">SummaryList</div>)
);
describe('PipelineSummary component tests', () => {
it('Component should render properly', () => {
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('If the pipeline url is not present in pipeline details, "-" should be displayed as pipeline url value', () => {
render(
<PipelineSummary
entityDetails={{ ...mockPipelineEntityDetails, pipelineUrl: undefined }}
/>,
{
wrapper: MemoryRouter,
}
);
const pipelineUrlValue = screen.getByTestId('pipeline-url-value');
expect(pipelineUrlValue).toContainHTML('-');
});
});

View File

@ -11,175 +11,60 @@
* limitations under the License.
*/
import { Col, Divider, Row, Space, Typography } from 'antd';
import { isEmpty } from 'lodash';
import React, { useMemo } from 'react';
import { Collapse, Row, Typography } from 'antd';
import { isEmpty, isUndefined } from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { ReactComponent as IconTagGrey } from '../../../../assets/svg/tag-grey.svg';
import { MAX_CHAR_LIMIT_ENTITY_SUMMARY } from '../../../../constants/constants';
import { getEntityName, getTagValue } from '../../../../utils/CommonUtils';
import SVGIcons from '../../../../utils/SvgUtils';
import { prepareConstraintIcon } from '../../../../utils/TableUtils';
import RichTextEditorPreviewer from '../../../common/rich-text-editor/RichTextEditorPreviewer';
import TagsViewer from '../../../tags-viewer/tags-viewer';
import { BasicColumnInfo, SummaryListProps } from './SummaryList.interface';
import { SummaryEntityType } from '../../../../enums/EntitySummary.enum';
import { SummaryListProps } from './SummaryList.interface';
import './SummaryList.style.less';
import SummaryListItems from './SummaryListItems/SummaryListItems.component';
const { Text, Paragraph } = Typography;
const { Text } = Typography;
export default function SummaryList({
columns,
charts,
tasks,
mlFeatures,
formattedEntityData,
entityType,
}: SummaryListProps) {
const { t } = useTranslation();
const formattedColumnsData: BasicColumnInfo[] = useMemo(() => {
if (columns) {
return columns.map((column) => ({
name: column.name,
title: <Text className="entity-title">{column.name}</Text>,
type: column.dataType,
tags: column.tags,
description: column.description,
constraint: column.constraint,
}));
} else if (charts) {
return charts.map((chart) => ({
name: chart.name,
title: (
<Link target="_blank" to={{ pathname: chart.chartUrl }}>
<Space className="m-b-xs">
<Text className="entity-title link">{getEntityName(chart)}</Text>
<SVGIcons alt="external-link" icon="external-link" width="12px" />
</Space>
</Link>
),
type: chart.chartType,
tags: chart.tags,
description: chart.description,
}));
} else if (tasks) {
return tasks.map((task) => ({
name: task.name,
title: (
<Link target="_blank" to={{ pathname: task.taskUrl }}>
<Space className="m-b-xs">
<Text className="entity-title link">{task.name}</Text>
<SVGIcons alt="external-link" icon="external-link" width="12px" />
</Space>
</Link>
),
type: task.taskType,
tags: task.tags,
description: task.description,
}));
} else if (mlFeatures) {
return mlFeatures.map((feature) => ({
algorithm: feature.featureAlgorithm,
name: feature.name || '--',
title: <Text className="entity-title">{feature.name}</Text>,
type: feature.dataType,
tags: feature.tags,
description: feature.description,
}));
} else {
return [];
}
}, [columns, charts, tasks, mlFeatures]);
return (
<Row>
{isEmpty(formattedColumnsData) ? (
{isEmpty(formattedEntityData) ? (
<div className="m-y-md">
<Text className="text-gray">{t('message.no-data-available')}</Text>
</div>
) : (
formattedColumnsData.map((entity) => (
<Col key={entity.name} span={24}>
<Row gutter={[0, 4]}>
<Col span={24}>
{columns &&
prepareConstraintIcon(
entity.name,
entity.constraint,
undefined,
'm-r-xss',
'14px'
)}
{entity.title}
</Col>
<Col span={24}>
<Row className="text-xs font-300" gutter={[4, 4]}>
<Col>
{entity.type && (
<Space size={4}>
<Text className="text-gray">{`${t(
'label.type'
)}:`}</Text>
<Text className="font-medium">{entity.type}</Text>
</Space>
)}
</Col>
{entity.algorithm && (
<>
<Col>
<Divider type="vertical" />
</Col>
<Col>
<Space size={4}>
<Text className="text-gray">{`${t(
'label.algorithm'
)}:`}</Text>
<Text className="font-medium">
{entity.algorithm}
</Text>
</Space>
</Col>
</>
)}
{entity.tags && entity.tags.length !== 0 && (
<>
<Col>
<Divider type="vertical" />
</Col>
<Col className="flex-grow">
<Space>
<IconTagGrey className="w-12 h-12" />
<Row wrap>
<TagsViewer
sizeCap={-1}
tags={(entity.tags || []).map((tag) =>
getTagValue(tag)
)}
/>
</Row>
</Space>
</Col>
</>
)}
</Row>
</Col>
<Col span={24}>
<Paragraph>
{entity.description ? (
<RichTextEditorPreviewer
markdown={entity.description || ''}
maxLength={MAX_CHAR_LIMIT_ENTITY_SUMMARY}
/>
) : (
t('label.no-entity', {
entity: t('label.description'),
})
)}
</Paragraph>
</Col>
</Row>
<Divider />
</Col>
))
formattedEntityData.map((entity) =>
isEmpty(entity.children) || isUndefined(entity.children) ? (
<SummaryListItems
entityDetails={entity}
isColumnsData={entityType === SummaryEntityType.COLUMN}
key={`${entity.name}-summary-list-item`}
/>
) : (
<Collapse
ghost
className="summary-list-collapse w-full"
collapsible="icon"
key={`${entity.name}-collapse`}>
<Collapse.Panel
data-testid={`${entity.name}-collapse`}
header={
<SummaryListItems
entityDetails={entity}
isColumnsData={entityType === SummaryEntityType.COLUMN}
/>
}
key={`${entity.name}-collapse-panel`}>
<SummaryList
entityType={entityType}
formattedEntityData={entity.children}
/>
</Collapse.Panel>
</Collapse>
)
)
)}
</Row>
);

View File

@ -12,27 +12,13 @@
*/
import { ReactNode } from 'react';
import { Chart, ChartType } from '../../../../generated/entity/data/chart';
import {
FeatureType,
MlFeature,
} from '../../../../generated/entity/data/mlmodel';
import { Task } from '../../../../generated/entity/data/pipeline';
import {
Column,
Constraint,
DataType,
} from '../../../../generated/entity/data/table';
import { SummaryEntityType } from '../../../../enums/EntitySummary.enum';
import { ChartType } from '../../../../generated/entity/data/chart';
import { FeatureType } from '../../../../generated/entity/data/mlmodel';
import { Constraint, DataType } from '../../../../generated/entity/data/table';
import { TagLabel } from '../../../../generated/type/tagLabel';
export interface SummaryListProps {
columns?: Column[];
charts?: Chart[];
tasks?: Task[];
mlFeatures?: MlFeature[];
}
export interface BasicColumnInfo {
export interface BasicEntityInfo {
algorithm?: string;
name: string;
title: ReactNode;
@ -40,4 +26,10 @@ export interface BasicColumnInfo {
tags?: TagLabel[];
description?: string;
constraint?: Constraint;
children?: BasicEntityInfo[];
}
export interface SummaryListProps {
formattedEntityData: BasicEntityInfo[];
entityType?: SummaryEntityType;
}

View File

@ -0,0 +1,30 @@
/*
* 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.
*/
.summary-list-collapse {
.ant-collapse-item {
.ant-collapse-header {
padding: 0px;
.ant-collapse-arrow {
margin-right: 8px;
}
}
.ant-collapse-content {
.ant-collapse-content-box {
padding: 0px;
padding-left: 20px;
}
}
}
}

View File

@ -0,0 +1,89 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render } from '@testing-library/react';
import React from 'react';
import { SummaryEntityType } from '../../../../enums/EntitySummary.enum';
import {
mockFormattedEntityData,
mockFormattedEntityDataWithChildren,
} from '../mocks/SummaryList.mock';
import SummaryList from './SummaryList.component';
jest.mock('./SummaryListItems/SummaryListItems.component', () =>
jest.fn().mockImplementation(({ entityDetails, isColumnsData }) => (
<div data-testid={`SummaryListItems-${entityDetails.name}`}>
<div>{entityDetails.name}</div>
<div data-testid={`isColumnsData-${entityDetails.name}`}>
{`${isColumnsData}`}
</div>
</div>
))
);
describe('SummaryList component tests', () => {
it('No data placeholder should display when an empty array is sent as a prop', () => {
const { getByText } = render(<SummaryList formattedEntityData={[]} />);
const noDataPlaceholder = getByText('message.no-data-available');
expect(noDataPlaceholder).toBeInTheDocument();
});
it('Summary list items should render properly for given formatted entity data', () => {
const { getByTestId } = render(
<SummaryList formattedEntityData={mockFormattedEntityData} />
);
const summaryListItem1 = getByTestId('SummaryListItems-name1');
const summaryListItem2 = getByTestId('SummaryListItems-name2');
const isColumnData1 = getByTestId('isColumnsData-name1');
const isColumnData2 = getByTestId('isColumnsData-name2');
expect(summaryListItem1).toBeInTheDocument();
expect(summaryListItem2).toBeInTheDocument();
expect(isColumnData1).toContainHTML('false');
expect(isColumnData2).toContainHTML('false');
});
it('SummaryListItem component should receive isColumnsData prop true for entityType "column"', () => {
const { getByTestId } = render(
<SummaryList
entityType={SummaryEntityType.COLUMN}
formattedEntityData={mockFormattedEntityData}
/>
);
const summaryListItem1 = getByTestId('SummaryListItems-name1');
const summaryListItem2 = getByTestId('SummaryListItems-name2');
const isColumnData1 = getByTestId('isColumnsData-name1');
const isColumnData2 = getByTestId('isColumnsData-name2');
expect(summaryListItem1).toBeInTheDocument();
expect(summaryListItem2).toBeInTheDocument();
expect(isColumnData1).toContainHTML('true');
expect(isColumnData2).toContainHTML('true');
});
it('Collapse should render for entity with children', () => {
const { getByTestId, queryByTestId } = render(
<SummaryList formattedEntityData={mockFormattedEntityDataWithChildren} />
);
const collapse1 = getByTestId('name1-collapse');
const collapse2 = queryByTestId('name2-collapse');
expect(collapse1).toBeInTheDocument();
expect(collapse2).toBeNull();
});
});

View File

@ -0,0 +1,120 @@
/*
* 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, Space, Typography } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as IconTagGrey } from '../../../../../assets/svg/tag-grey.svg';
import { MAX_CHAR_LIMIT_ENTITY_SUMMARY } from '../../../../../constants/constants';
import { getTagValue } from '../../../../../utils/CommonUtils';
import { prepareConstraintIcon } from '../../../../../utils/TableUtils';
import RichTextEditorPreviewer from '../../../../common/rich-text-editor/RichTextEditorPreviewer';
import TagsViewer from '../../../../tags-viewer/tags-viewer';
import { SummaryListItemProps } from './SummaryListItems.interface';
const { Text, Paragraph } = Typography;
function SummaryListItem({
entityDetails,
isColumnsData,
}: SummaryListItemProps) {
const { t } = useTranslation();
return (
<Col key={entityDetails.name} span={24}>
<Row gutter={[0, 4]}>
<Col data-testid="title-container" span={24}>
{isColumnsData &&
prepareConstraintIcon(
entityDetails.name,
entityDetails.constraint,
undefined,
'm-r-xss',
'14px'
)}
{entityDetails.title}
</Col>
<Col span={24}>
<Row className="text-xs font-300" gutter={[4, 4]}>
<Col>
{entityDetails.type && (
<Space size={4}>
<Text className="text-gray">{`${t('label.type')}:`}</Text>
<Text className="font-medium" data-testid="entity-type">
{entityDetails.type}
</Text>
</Space>
)}
</Col>
{entityDetails.algorithm && (
<>
<Col>
<Divider type="vertical" />
</Col>
<Col>
<Space size={4}>
<Text className="text-gray">{`${t(
'label.algorithm'
)}:`}</Text>
<Text className="font-medium" data-testid="algorithm">
{entityDetails.algorithm}
</Text>
</Space>
</Col>
</>
)}
{entityDetails.tags && entityDetails.tags.length !== 0 && (
<>
<Col>
<Divider type="vertical" />
</Col>
<Col className="flex-grow">
<Space>
<IconTagGrey
className="w-12 h-12"
data-testid="tag-grey-icon"
/>
<Row wrap>
<TagsViewer
sizeCap={-1}
tags={(entityDetails.tags || []).map((tag) =>
getTagValue(tag)
)}
/>
</Row>
</Space>
</Col>
</>
)}
</Row>
</Col>
<Col span={24}>
<Paragraph>
{entityDetails.description ? (
<RichTextEditorPreviewer
markdown={entityDetails.description || ''}
maxLength={MAX_CHAR_LIMIT_ENTITY_SUMMARY}
/>
) : (
t('label.no-entity', { entity: t('label.description') })
)}
</Paragraph>
</Col>
</Row>
<Divider />
</Col>
);
}
export default SummaryListItem;

View File

@ -0,0 +1,19 @@
/*
* 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 { BasicEntityInfo } from '../SummaryList.interface';
export interface SummaryListItemProps {
entityDetails: BasicEntityInfo;
isColumnsData?: boolean;
}

View File

@ -0,0 +1,133 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render } from '@testing-library/react';
import React from 'react';
import {
mockEntityDetails,
mockEntityDetailsWithConstraint,
mockEntityDetailsWithoutDescription,
mockEntityDetailsWithTagsAndAlgorithm,
} from '../../mocks/SummaryListItems.mock';
import SummaryListItem from './SummaryListItems.component';
jest.mock('../../../../common/rich-text-editor/RichTextEditorPreviewer', () =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="RichTextEditorPreviewer">RichTextEditorPreviewer</div>
))
);
jest.mock('../../../../tags-viewer/tags-viewer', () =>
jest
.fn()
.mockImplementation(() => <div data-testid="TagsViewer">TagsViewer</div>)
);
jest.mock('../../../../../utils/TableUtils', () => ({
prepareConstraintIcon: jest
.fn()
.mockImplementation(() => <div data-testid="constraints">Constraints</div>),
}));
describe('SummaryListItems component tests', () => {
it('Component should render properly with title, type and description of the entity', () => {
const { getByTestId } = render(
<SummaryListItem entityDetails={mockEntityDetails} />
);
const titleContainer = getByTestId('title-container');
const title = getByTestId('title');
const type = getByTestId('entity-type');
const description = getByTestId('RichTextEditorPreviewer');
expect(titleContainer).toBeInTheDocument();
expect(title).toContainHTML('Title');
expect(type).toBeInTheDocument();
expect(type).toContainHTML(mockEntityDetails.type);
expect(description).toBeInTheDocument();
});
it('tags and algorithm should not render if entity has no tags and algorithm', () => {
const { queryByTestId } = render(
<SummaryListItem entityDetails={mockEntityDetails} />
);
const tags = queryByTestId('TagsViewer');
const algorithm = queryByTestId('algorithm');
expect(tags).toBeNull();
expect(algorithm).toBeNull();
});
it('tags and algorithm should render if entity has tags and algorithm', () => {
const { getByTestId } = render(
<SummaryListItem entityDetails={mockEntityDetailsWithTagsAndAlgorithm} />
);
const tags = getByTestId('TagsViewer');
const algorithm = getByTestId('algorithm');
expect(tags).toBeInTheDocument();
expect(algorithm).toBeInTheDocument();
expect(algorithm).toContainHTML(
mockEntityDetailsWithTagsAndAlgorithm.algorithm
);
});
it('no description placeholder should be displayed for entity having no description', async () => {
const { getByText, queryByTestId } = render(
<SummaryListItem entityDetails={mockEntityDetailsWithoutDescription} />
);
const richTextEditorPreviewer = queryByTestId('RichTextEditorPreviewer');
const noDescription = getByText('label.no-entity');
expect(richTextEditorPreviewer).toBeNull();
expect(noDescription).toBeInTheDocument();
});
it('constraint icon should not be present for entity without constraint details', async () => {
const { queryByTestId } = render(
<SummaryListItem entityDetails={mockEntityDetails} />
);
const richTextEditorPreviewer = queryByTestId('constraints');
expect(richTextEditorPreviewer).toBeNull();
});
it('constraint icon should not be present for entity if isColumnsData prop is not true', async () => {
const { queryByTestId } = render(
<SummaryListItem entityDetails={mockEntityDetailsWithConstraint} />
);
const richTextEditorPreviewer = queryByTestId('constraints');
expect(richTextEditorPreviewer).toBeNull();
});
it('constraint icon should be displayed for entity with constraint details', async () => {
const { getByTestId } = render(
<SummaryListItem
isColumnsData
entityDetails={mockEntityDetailsWithConstraint}
/>
);
const richTextEditorPreviewer = getByTestId('constraints');
expect(richTextEditorPreviewer).toBeInTheDocument();
});
});

View File

@ -24,6 +24,7 @@ import {
import { getListTestCase } from 'rest/testAPI';
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 { Include } from '../../../../generated/type/include';
@ -32,6 +33,7 @@ import {
formTwoDigitNmber,
} 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';
@ -40,6 +42,7 @@ import {
TableTestsType,
} from '../../../TableProfiler/TableProfiler.interface';
import SummaryList from '../SummaryList/SummaryList.component';
import { BasicEntityInfo } from '../SummaryList/SummaryList.interface';
import { BasicTableInfo, TableSummaryProps } from './TableSummary.interface';
function TableSummary({ entityDetails }: TableSummaryProps) {
@ -162,6 +165,11 @@ function TableSummary({ entityDetails }: TableSummaryProps) {
[tableType, columns, tableQueries]
);
const formattedColumnsData: BasicEntityInfo[] = useMemo(
() => getFormattedEntityData(SummaryEntityType.COLUMN, columns),
[columns]
);
useEffect(() => {
if (!isEmpty(entityDetails)) {
setTableDetails(entityDetails);
@ -185,10 +193,13 @@ function TableSummary({ entityDetails }: TableSummaryProps) {
{Object.keys(basicTableInfo).map((fieldName) => (
<Col key={fieldName} span={24}>
<Row gutter={16}>
<Col className="text-gray" span={10}>
<Col
className="text-gray"
data-testid={`${fieldName}-label`}
span={10}>
{fieldName}
</Col>
<Col span={12}>
<Col data-testid={`${fieldName}-value`} span={12}>
{basicTableInfo[fieldName as keyof BasicTableInfo]}
</Col>
</Row>
@ -201,13 +212,15 @@ function TableSummary({ entityDetails }: TableSummaryProps) {
<Row className={classNames('m-md')} gutter={[0, 16]}>
<Col span={24}>
<Typography.Text className="section-header">
<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>
<Typography.Text data-testid="no-profiler-enabled-message">
{t('message.no-profiler-enabled-summary-message')}
</Typography.Text>
) : (
@ -216,7 +229,9 @@ function TableSummary({ entityDetails }: TableSummaryProps) {
<Col key={field.title} span={10}>
<Row>
<Col span={24}>
<Typography.Text className="text-gray">
<Typography.Text
className="text-gray"
data-testid={`${field.title}-label`}>
{field.title}
</Typography.Text>
</Col>
@ -225,7 +240,8 @@ function TableSummary({ entityDetails }: TableSummaryProps) {
className={classNames(
'summary-panel-statistics-count',
field.className
)}>
)}
data-testid={`${field.title}-value`}>
{field.value}
</Typography.Text>
</Col>
@ -239,12 +255,17 @@ function TableSummary({ entityDetails }: TableSummaryProps) {
<Divider className="m-0" />
<Row className={classNames('m-md')} gutter={[0, 16]}>
<Col span={24}>
<Typography.Text className="section-header">
<Typography.Text
className="section-header"
data-testid="schema-header">
{t('label.schema')}
</Typography.Text>
</Col>
<Col span={24}>
<SummaryList columns={columns} />
<SummaryList
entityType={SummaryEntityType.COLUMN}
formattedEntityData={formattedColumnsData}
/>
</Col>
</Row>
</>

View File

@ -0,0 +1,136 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { act, render, screen } from '@testing-library/react';
import React from 'react';
import { getLatestTableProfileByFqn } from 'rest/tableAPI';
import { mockTableEntityDetails } from '../mocks/TableSummary.mock';
import TableSummary from './TableSummary.component';
jest.mock('rest/testAPI', () => ({
getListTestCase: jest.fn().mockReturnValue([]),
}));
jest.mock('rest/tableAPI', () => ({
getLatestTableProfileByFqn: jest
.fn()
.mockImplementation(() => mockTableEntityDetails),
getTableQueryByTableId: jest
.fn()
.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()
.mockImplementation(() => <div data-testid="SummaryList">SummaryList</div>)
);
describe('TableSummary component tests', () => {
it('Component should render properly', 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 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(columnsValue).toContainHTML('2');
expect(noProfilerPlaceholder).toContainHTML(
'message.no-profiler-enabled-summary-message'
);
expect(summaryList).toBeInTheDocument();
});
it('Profiler data should be displayed for tables with profiler data available', async () => {
(getLatestTableProfileByFqn as jest.Mock).mockImplementationOnce(() => ({
...mockTableEntityDetails,
profile: { rowCount: 30, columnCount: 2, timestamp: 38478857 },
}));
await act(async () => {
render(<TableSummary entityDetails={mockTableEntityDetails} />);
});
const rowCountLabel = screen.getByTestId('label.row-count-label');
const colCountLabel = screen.getByTestId('label.column-entity-label');
const tableSampleLabel = screen.getByTestId(
'label.table-entity-text %-label'
);
const testsPassedLabel = screen.getByTestId(
'label.test-plural label.passed-label'
);
const testsAbortedLabel = screen.getByTestId(
'label.test-plural label.aborted-label'
);
const testsFailedLabel = screen.getByTestId(
'label.test-plural label.failed-label'
);
const rowCountValue = screen.getByTestId('label.row-count-value');
const colCountValue = screen.getByTestId('label.column-entity-value');
const tableSampleValue = screen.getByTestId(
'label.table-entity-text %-value'
);
const testsPassedValue = screen.getByTestId(
'label.test-plural label.passed-value'
);
const testsAbortedValue = screen.getByTestId(
'label.test-plural label.aborted-value'
);
const testsFailedValue = screen.getByTestId(
'label.test-plural label.failed-value'
);
expect(rowCountLabel).toBeInTheDocument();
expect(colCountLabel).toBeInTheDocument();
expect(tableSampleLabel).toBeInTheDocument();
expect(testsPassedLabel).toBeInTheDocument();
expect(testsAbortedLabel).toBeInTheDocument();
expect(testsFailedLabel).toBeInTheDocument();
expect(rowCountValue).toContainHTML('30');
expect(colCountValue).toContainHTML('2');
expect(tableSampleValue).toContainHTML('100%');
expect(testsPassedValue).toContainHTML('00');
expect(testsAbortedValue).toContainHTML('00');
expect(testsFailedValue).toContainHTML('00');
});
});

View File

@ -13,15 +13,20 @@
import { Col, Divider, Row, Typography } from 'antd';
import { isArray } from 'lodash';
import React, { useMemo } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getTopicByFqn } from 'rest/topicsAPI';
import { SummaryEntityType } from '../../../../enums/EntitySummary.enum';
import { SearchIndex } from '../../../../enums/search.enum';
import { 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 SchemaEditor from '../../../schema-editor/SchemaEditor';
import { TopicConfigObjectInterface } from '../../../TopicDetails/TopicDetails.interface';
import SummaryList from '../SummaryList/SummaryList.component';
import { BasicEntityInfo } from '../SummaryList/SummaryList.interface';
interface TopicSummaryProps {
entityDetails: Topic;
@ -29,15 +34,49 @@ interface TopicSummaryProps {
function TopicSummary({ entityDetails }: TopicSummaryProps) {
const { t } = useTranslation();
const [topicDetails, setTopicDetails] = useState<Topic>(entityDetails);
const topicConfig = useMemo(() => {
const configs = getConfigObject(entityDetails);
const configs = getConfigObject(topicDetails);
return {
...configs,
'Retention Size': bytesToSize(configs['Retention Size'] ?? 0),
'Max Message Size': bytesToSize(configs['Max Message Size'] ?? 0),
};
}, [topicDetails]);
const formattedSchemaFieldsData: BasicEntityInfo[] = useMemo(
() =>
getFormattedEntityData(
SummaryEntityType.SCHEMAFIELD,
topicDetails.messageSchema?.schemaFields
),
[topicDetails]
);
const fetchExtraTopicInfo = async () => {
try {
const res = await getTopicByFqn(
entityDetails.fullyQualifiedName ?? '',
''
);
const { partitions } = res;
setTopicDetails({ ...entityDetails, partitions });
} catch {
showErrorToast(
t('server.entity-details-fetch-error', {
entityType: t('label.topic-lowercase'),
entityName: entityDetails.name,
})
);
}
};
useEffect(() => {
fetchExtraTopicInfo();
}, [entityDetails]);
return (
@ -56,14 +95,19 @@ function TopicSummary({ entityDetails }: TopicSummaryProps) {
const value =
topicConfig[fieldName as keyof TopicConfigObjectInterface];
const fieldValue = isArray(value) ? value.join(', ') : value;
return (
<Col key={fieldName} span={24}>
<Row gutter={16}>
<Col className="text-gray" span={10}>
<Col
className="text-gray"
data-testid={`${fieldName}-label`}
span={10}>
{fieldName}
</Col>
<Col span={12}>
{isArray(value) ? value.join(', ') : value}
<Col data-testid={`${fieldName}-value`} span={12}>
{fieldValue ? fieldValue : '-'}
</Col>
</Row>
</Col>
@ -73,21 +117,22 @@ function TopicSummary({ entityDetails }: TopicSummaryProps) {
</Col>
</Row>
<Divider className="m-0" />
<Row className="m-md">
<Row className="m-md" gutter={[0, 16]}>
<Col span={24}>
<Typography.Text className="section-header">
<Typography.Text
className="section-header"
data-testid="schema-header">
{t('label.schema')}
</Typography.Text>
</Col>
<Col span={24}>
{entityDetails.messageSchema?.schemaText ? (
<SchemaEditor
editorClass="summary-schema-editor"
value={entityDetails.messageSchema.schemaText}
/>
{entityDetails.messageSchema?.schemaFields ? (
<SummaryList formattedEntityData={formattedSchemaFieldsData} />
) : (
<div className="m-y-md">
<Typography.Text className="text-gray">
<Typography.Text
className="text-gray"
data-testid="no-data-message">
{t('message.no-data-available')}
</Typography.Text>
</div>

View File

@ -0,0 +1,111 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { act, render, screen } from '@testing-library/react';
import React from 'react';
import { getTopicByFqn } from 'rest/topicsAPI';
import { mockTopicEntityDetails } 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()
.mockImplementation(() => <div data-testid="SummaryList">SummaryList</div>)
);
jest.mock('rest/topicsAPI', () => ({
getTopicByFqn: jest.fn().mockImplementation(() => ({ partitions: 128 })),
}));
describe('TopicSummary component tests', () => {
it('Component should render properly', async () => {
await act(async () => {
render(<TopicSummary entityDetails={mockTopicEntityDetails} />);
});
const topicTitle = screen.getByTestId('TableDataCardTitle');
const partitionsLabel = screen.getByTestId('Partitions-label');
const replicationFactorLabel = screen.getByTestId(
'Replication Factor-label'
);
const retentionSizeLabel = screen.getByTestId('Retention Size-label');
const cleanUpPoliciesLabel = screen.getByTestId('CleanUp Policies-label');
const maxMessageSizeLabel = screen.getByTestId('Max Message Size-label');
const partitionsValue = screen.getByTestId('Partitions-value');
const replicationFactorValue = screen.getByTestId(
'Replication Factor-value'
);
const retentionSizeValue = screen.getByTestId('Retention Size-value');
const cleanUpPoliciesValue = screen.getByTestId('CleanUp Policies-value');
const maxMessageSizeValue = screen.getByTestId('Max Message Size-value');
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(replicationFactorValue).toContainHTML('4');
expect(retentionSizeValue).toContainHTML('1018.83 MB');
expect(cleanUpPoliciesValue).toContainHTML('delete');
expect(maxMessageSizeValue).toContainHTML('208 Bytes');
expect(schemaHeader).toBeInTheDocument();
expect(summaryList).toBeInTheDocument();
});
it('No data message should be shown in case not schemaFields are available in topic details', async () => {
await act(async () => {
render(
<TopicSummary
entityDetails={{
...mockTopicEntityDetails,
messageSchema: {
...mockTopicEntityDetails.messageSchema,
schemaFields: undefined,
},
}}
/>
);
});
const summaryList = screen.queryByTestId('SummaryList');
const noDataMessage = screen.queryByTestId('no-data-message');
expect(summaryList).toBeNull();
expect(noDataMessage).toBeInTheDocument();
});
it('In case any topic field is not present, "-" should be displayed in place of value', async () => {
(getTopicByFqn as jest.Mock).mockImplementationOnce(() => Promise.reject());
await act(async () => {
render(<TopicSummary entityDetails={mockTopicEntityDetails} />);
});
const partitionsValue = screen.getByTestId('Partitions-value');
expect(partitionsValue).toContainHTML('-');
});
});

View File

@ -0,0 +1,99 @@
/*
* 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 {
Dashboard,
DashboardServiceType,
} from '../../../../generated/entity/data/dashboard';
const mockDate = new Date('2023-01-03');
export const mockDashboardEntityDetails: Dashboard = {
id: '2edaff89-b1d4-47b6-a081-d72f08e1def9',
name: 'deck.gl Demo',
displayName: 'deck.gl Demo',
fullyQualifiedName: 'sample_superset.10',
description: '',
version: 0.1,
updatedAt: 1672627828951,
updatedBy: 'admin',
dashboardUrl: 'http://localhost:808/superset/dashboard/deck/',
charts: [
{
id: 'eba9c260-4036-4c57-92fe-6c6e3d703bda',
type: 'chart',
name: '127',
fullyQualifiedName: 'sample_superset.127',
description: '',
displayName: 'Are you an ethnic minority in your city?',
deleted: false,
href: 'http://openmetadata-server:8585/api/v1/charts/eba9c260-4036-4c57-92fe-6c6e3d703bda',
},
],
href: 'http://openmetadata-server:8585/api/v1/dashboards/2edaff89-b1d4-47b6-a081-d72f08e1def9',
followers: [],
service: {
id: '38ae6d66-7086-4e00-b2d6-cabd2b951993',
type: 'dashboardService',
name: 'sample_superset',
fullyQualifiedName: 'sample_superset',
deleted: false,
href: 'http://openmetadata-server:8585/api/v1/services/dashboardServices/38ae6d66-7086-4e00-b2d6-cabd2b951993',
},
serviceType: DashboardServiceType.Superset,
usageSummary: {
dailyStats: {
count: 0,
percentileRank: 0,
},
weeklyStats: {
count: 0,
percentileRank: 0,
},
monthlyStats: {
count: 0,
percentileRank: 0,
},
date: mockDate,
},
deleted: false,
tags: [],
};
export const mockFetchChartsResponse = [
{
id: 'eba9c260-4036-4c57-92fe-6c6e3d703bda',
name: '127',
displayName: 'Are you an ethnic minority in your city?',
fullyQualifiedName: 'sample_superset.127',
description: '',
version: 0.1,
updatedAt: 1672627828742,
updatedBy: 'admin',
chartType: 'Other',
chartUrl:
'http://localhost:8088/superset/explore/?form_data=%7B%22slice_id%22%3A%20127%7D',
href: 'http://localhost:8585/api/v1/charts/eba9c260-4036-4c57-92fe-6c6e3d703bda',
tags: [],
service: {
id: '38ae6d66-7086-4e00-b2d6-cabd2b951993',
type: 'dashboardService',
name: 'sample_superset',
fullyQualifiedName: 'sample_superset',
deleted: false,
href: 'http://localhost:8585/api/v1/services/dashboardServices/38ae6d66-7086-4e00-b2d6-cabd2b951993',
},
serviceType: 'Superset',
deleted: false,
},
];

View File

@ -0,0 +1,146 @@
/*
* 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 {
FeatureSourceDataType,
FeatureType,
Mlmodel,
} from '../../../../generated/entity/data/mlmodel';
export const mockMlModelEntityDetails: Mlmodel = {
id: 'e42a5d43-36fd-4636-ae89-5bcc6a61542e',
name: 'eta_predictions',
displayName: 'ETA Predictions',
fullyQualifiedName: 'mlflow_svc.eta_predictions',
description: 'ETA Predictions Model',
version: 0.1,
updatedAt: 1672627829904,
updatedBy: 'admin',
algorithm: 'Neural Network',
mlFeatures: [
{
name: 'sales',
dataType: FeatureType.Numerical,
description: 'Sales amount',
fullyQualifiedName: 'mlflow_svc.eta_predictions.sales',
featureSources: [
{
name: 'gross_sales',
dataType: FeatureSourceDataType.Integer,
fullyQualifiedName:
'sample_data.ecommerce_db.shopify.fact_sale.gross_sales',
dataSource: {
id: '148e01f8-817a-4094-aa39-970746e3427e',
type: 'table',
fullyQualifiedName: 'sample_data.ecommerce_db.shopify.fact_sale',
description:
'The fact table captures the value of products sold or returned.',
href: 'http://openmetadata-server:8585/api/v1/tables/148e01f8-817a-4094-aa39-970746e3427e',
},
},
],
},
{
name: 'persona',
dataType: FeatureType.Categorical,
description: 'type of buyer',
fullyQualifiedName: 'mlflow_svc.eta_predictions.persona',
featureSources: [
{
name: 'membership',
dataType: FeatureSourceDataType.String,
fullyQualifiedName:
'sample_data.ecommerce_db.shopify.raw_customer.membership',
dataSource: {
id: 'e2e27b89-9eda-441f-afe4-3c9b780e9e15',
type: 'table',
fullyQualifiedName: 'sample_data.ecommerce_db.shopify.raw_customer',
description:
'This is a raw customers table as represented in our online DB. ',
href: 'http://openmetadata-server:8585/api/v1/tables/e2e27b89-9eda-441f-afe4-3c9b780e9e15',
},
},
{
name: 'platform',
dataType: FeatureSourceDataType.String,
fullyQualifiedName:
'sample_data.ecommerce_db.shopify.raw_customer.platform',
dataSource: {
id: 'e2e27b89-9eda-441f-afe4-3c9b780e9e15',
type: 'table',
fullyQualifiedName: 'sample_data.ecommerce_db.shopify.raw_customer',
description:
'This is a raw customers table as represented in our online DB. This contains personal.',
href: 'http://openmetadata-server:8585/api/v1/tables/e2e27b89-9eda-441f-afe4-3c9b780e9e15',
},
},
],
featureAlgorithm: 'PCA',
},
],
mlHyperParameters: [
{
name: 'regularisation',
value: '0.5',
},
{
name: 'random',
value: 'hello',
},
],
dashboard: {
name: 'DashboardName',
id: '4352345234534538992643452345',
type: '',
},
target: 'ETA_time',
mlStore: {
storage: 's3://path-to-pickle',
imageRepository: 'https://docker.hub.com/image',
},
service: {
name: 'MLFlow',
id: '43523452345345325423452345',
type: '',
},
server: 'http://my-server.ai',
tags: [],
followers: [],
href: 'http://openmetadata-server:8585/api/v1/mlmodels/e42a5d43-36fd-4636-ae89-5bcc6a61542e',
deleted: false,
};
export const mockMlModelEntityDetails1: Mlmodel = {
id: 'b849cc70-ceda-4f2a-8de2-022a5c7f78a6',
name: 'forecast_sales',
displayName: 'Sales Forecast Predictions',
fullyQualifiedName: 'mlflow_svc.forecast_sales',
description: 'Sales Forecast Predictions Model',
version: 0.1,
updatedAt: 1672627829947,
updatedBy: 'admin',
algorithm: 'Time Series',
mlFeatures: [],
mlHyperParameters: [],
target: '',
server: '',
service: {
name: 'MLFlow',
id: '43523452345345325423452345',
type: '',
},
tags: [],
followers: [],
href: 'http://openmetadata-server:8585/api/v1/mlmodels/b849cc70-ceda-4f2a-8de2-022a5c7f78a6',
deleted: false,
};

View File

@ -0,0 +1,59 @@
/*
* 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 { Pipeline } from '../../../../generated/entity/data/pipeline';
export const mockPipelineEntityDetails: Pipeline = {
id: 'b35f7a53-16a9-4ed1-9223-801c3d75674f',
name: 'dim_address_etl',
displayName: 'dim_address etl',
fullyQualifiedName: 'sample_airflow.dim_address_etl',
description: 'dim_address ETL pipeline',
version: 0.1,
updatedAt: 1672627829327,
updatedBy: 'admin',
pipelineUrl: 'http://localhost:8080/tree?dag_id=dim_address_etl',
tasks: [
{
name: 'dim_address_task',
displayName: 'dim_address Task',
description:
'Airflow operator to perform ETL and generate dim_address table',
taskUrl:
'http://localhost:8080/taskinstance/list/?flt1_dag_id_equals=dim_address_task',
downstreamTasks: ['assert_table_exists'],
taskType: 'PrestoOperator',
},
{
name: 'assert_table_exists',
displayName: 'Assert Table Exists',
description: 'Assert if a table exists',
taskUrl:
'http://localhost:8080/taskinstance/list/?flt1_dag_id_equals=assert_table_exists',
downstreamTasks: [],
taskType: 'HiveOperator',
},
],
deleted: false,
href: 'http://openmetadata-server:8585/api/v1/pipelines/b35f7a53-16a9-4ed1-9223-801c3d75674f',
followers: [],
tags: [],
service: {
id: 'd1c5f7b4-dc61-4336-a4b1-a27e0b97d791',
type: 'pipelineService',
name: 'sample_airflow',
fullyQualifiedName: 'sample_airflow',
deleted: false,
href: 'http://openmetadata-server:8585/api/v1/services/pipelineServices/d1c5f7b4-dc61-4336-a4b1-a27e0b97d791',
},
};

View File

@ -0,0 +1,64 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const mockFormattedEntityData = [
{
children: [],
constraint: undefined,
description: 'Description for name1',
name: 'name1',
tags: [],
title: 'Title1',
type: 'ARRAY',
},
{
children: [],
constraint: undefined,
description: 'Description for name2',
name: 'name2',
tags: [],
title: 'Title2',
type: 'OBJECT',
},
];
export const mockFormattedEntityDataWithChildren = [
{
children: [
{
children: [],
constraint: undefined,
description: 'Description for child1',
name: 'child1',
tags: [],
title: 'ChildTitle2',
type: 'OBJECT',
},
],
constraint: undefined,
description: 'Description for name1',
name: 'name1',
tags: [],
title: 'Title1',
type: 'ARRAY',
},
{
children: [],
constraint: undefined,
description: 'Description for name2',
name: 'name2',
tags: [],
title: 'Title2',
type: 'OBJECT',
},
];

View File

@ -0,0 +1,69 @@
/*
* 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 React from 'react';
import { Constraint } from '../../../../generated/entity/data/table';
import {
LabelType,
State,
TagSource,
} from '../../../../generated/type/tagLabel';
export const mockEntityDetails = {
children: [],
constraint: undefined,
description: 'Description for shipping_address',
name: 'shipping_address',
tags: [],
title: <div data-testid="title">Title</div>,
type: 'ARRAY',
};
export const mockEntityDetailsWithTagsAndAlgorithm = {
children: [],
algorithm: 'The Algo',
constraint: undefined,
description: undefined,
name: 'shipping_address',
tags: [
{
tagFQN: 'PersonalData.SpecialCategory',
labelType: LabelType.Manual,
description: 'Test Description',
source: TagSource.Tag,
state: State.Confirmed,
},
],
title: <div data-testid="title">Title</div>,
type: 'ARRAY',
};
export const mockEntityDetailsWithoutDescription = {
children: [],
constraint: undefined,
description: undefined,
name: 'shipping_address',
tags: [],
title: <div data-testid="title">Title</div>,
type: 'ARRAY',
};
export const mockEntityDetailsWithConstraint = {
children: [],
constraint: Constraint.PrimaryKey,
description: undefined,
name: 'shipping_address',
tags: [],
title: <div data-testid="title">Title</div>,
type: 'ARRAY',
};

View File

@ -0,0 +1,101 @@
/*
* 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 {
DatabaseServiceType,
DataType,
LabelType,
State,
Table,
TagSource,
} from '../../../../generated/entity/data/table';
const mockDate = new Date('2023-01-03');
export const mockTableEntityDetails: Table = {
id: '8dd1f238-6ba0-46c6-a091-7db81f2a6bed',
name: 'dim.api/client',
fullyQualifiedName: 'sample_data.ecommerce_db.shopify."dim.api/client"',
description:
'This dimension table contains a row for each channel or app that your customers use to create orders. ',
displayName: 'dim.api/client',
version: 0.2,
updatedAt: 1672668265493,
updatedBy: 'admin',
href: 'http://openmetadata-server:8585/api/v1/tables/8dd1f238-6ba0-46c6-a091-7db81f2a6bed',
columns: [
{
name: 'api_client_id',
dataType: DataType.Numeric,
dataTypeDisplay: 'numeric',
description:
'ID of the API client that called the Shopify API. For example, the ID for the online store is 580111.',
fullyQualifiedName:
'sample_data.ecommerce_db.shopify."dim.api/client".api_client_id',
tags: [
{
tagFQN: 'PersonalData.SpecialCategory',
description:
'GDPR special category data is personal information of data subjects that is especially sensitive.',
source: TagSource.Tag,
labelType: LabelType.Manual,
state: State.Confirmed,
},
],
ordinalPosition: 1,
},
{
name: 'title',
dataType: DataType.Varchar,
dataLength: 100,
dataTypeDisplay: 'varchar',
description:
'Full name of the app or channel. For example, Point of Sale, Online Store.',
fullyQualifiedName:
'sample_data.ecommerce_db.shopify."dim.api/client".title',
tags: [],
ordinalPosition: 2,
},
],
deleted: false,
serviceType: DatabaseServiceType.BigQuery,
tags: [
{
tagFQN: 'PersonalData.SpecialCategory',
description:
'GDPR special category data is personal information of data subjects that is especially sensitive.',
source: TagSource.Tag,
labelType: LabelType.Manual,
state: State.Confirmed,
},
],
tableQueries: [
{
query:
'select cust.customer_id, fact_order.order_id from dim_customer cust join fact_order on',
users: [],
vote: 1,
checksum: 'ff727cf70d5a7a9810704532f3571b82',
queryDate: mockDate,
},
{
query:
'select sale.sale_id, cust.customer_id, fact_order.order_ir from shopify.',
users: [],
vote: 1,
checksum: 'e14e02c387dd8482d10c4ec7d3d4c69a',
queryDate: mockDate,
},
],
followers: [],
};

View File

@ -0,0 +1,100 @@
/*
* 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 {
CleanupPolicy,
DataTypeTopic,
SchemaType,
Topic,
} from '../../../../generated/entity/data/topic';
export const mockTopicEntityDetails: Topic = {
id: '67e1dbbb-054c-4833-ba28-f95d71f0826f',
name: 'product_events',
displayName: 'product_events',
fullyQualifiedName: 'sample_kafka.product_events',
description:
'Kafka topic to capture the product events. This topic will get updates on products decription, price etc.',
version: 0.1,
updatedAt: 1672627828429,
updatedBy: 'admin',
href: 'http://openmetadata-server:8585/api/v1/topics/67e1dbbb-054c-4833-ba28-f95d71f0826f',
deleted: false,
service: {
id: '5d6f73f0-1811-49c8-8d1d-7a478ffd8177',
type: 'messagingService',
name: 'sample_kafka',
fullyQualifiedName: 'sample_kafka',
deleted: false,
href: 'http://openmetadata-server:8585/api/v1/services/messagingServices/5d6f73f0-1811-49c8-8d1d-7a478ffd8177',
},
messageSchema: {
schemaText:
'{"namespace":"openmetadata.kafka","type":"record","name":"Product","fields":[{"name":"product_id","type":"int"}]}',
schemaType: SchemaType.Avro,
schemaFields: [
{
name: 'Product',
dataType: DataTypeTopic.Record,
fullyQualifiedName: 'sample_kafka.product_events.Product',
tags: [],
children: [
{
name: 'product_id',
dataType: DataTypeTopic.Int,
fullyQualifiedName:
'sample_kafka.product_events.Product.product_id',
tags: [],
},
{
name: 'title',
dataType: DataTypeTopic.String,
fullyQualifiedName: 'sample_kafka.product_events.Product.title',
tags: [],
},
{
name: 'price',
dataType: DataTypeTopic.Double,
fullyQualifiedName: 'sample_kafka.product_events.Product.price',
tags: [],
},
{
name: 'sku',
dataType: DataTypeTopic.String,
fullyQualifiedName: 'sample_kafka.product_events.Product.sku',
tags: [],
},
{
name: 'barcode',
dataType: DataTypeTopic.String,
fullyQualifiedName: 'sample_kafka.product_events.Product.barcode',
tags: [],
},
{
name: 'shop_id',
dataType: DataTypeTopic.Int,
fullyQualifiedName: 'sample_kafka.product_events.Product.shop_id',
tags: [],
},
],
},
],
},
partitions: 0,
cleanupPolicies: [CleanupPolicy.Delete],
replicationFactor: 4,
maximumMessageSize: 208,
retentionSize: 1068320655,
tags: [],
followers: [],
};

View File

@ -23,6 +23,7 @@ import { Thread, ThreadType } from '../../generated/entity/feed/thread';
import { EntityLineage } from '../../generated/type/entityLineage';
import { EntityReference } from '../../generated/type/entityReference';
import { Paging } from '../../generated/type/paging';
import { SchemaType } from '../../generated/type/schema';
import { TagLabel } from '../../generated/type/tagLabel';
import {
EntityFieldThreadCount,
@ -102,4 +103,5 @@ export interface TopicConfigObjectInterface {
'Retention Size'?: number;
'CleanUp Policies'?: CleanupPolicy[];
'Max Message Size'?: number;
'Schema Type'?: SchemaType;
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export enum SummaryEntityType {
COLUMN = 'column',
CHART = 'chart',
TASK = 'task',
MLFEATURE = 'mlFeature',
SCHEMAFIELD = 'schemaField',
}

View File

@ -0,0 +1,61 @@
/*
* 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 { SummaryEntityType } from '../enums/EntitySummary.enum';
import { Column } from '../generated/entity/data/table';
import { getFormattedEntityData } from './EntitySummaryPanelUtils';
import {
mockEntityDataWithNesting,
mockEntityDataWithNestingResponse,
mockEntityDataWithoutNesting,
mockEntityDataWithoutNestingResponse,
mockInvalidDataResponse,
} from './mocks/EntitySummaryPanelUtils.mock';
describe('EntitySummaryPanelUtils tests', () => {
it('getFormattedEntityData should return formatted data properly for table columns data without nesting', () => {
const resultFormattedData = getFormattedEntityData(
SummaryEntityType.COLUMN,
mockEntityDataWithoutNesting
);
expect(resultFormattedData).toEqual(mockEntityDataWithoutNestingResponse);
});
it('getFormattedEntityData should return formatted data properly for topic fields data with nesting', () => {
const resultFormattedData = getFormattedEntityData(
SummaryEntityType.COLUMN,
mockEntityDataWithNesting
);
expect(resultFormattedData).toEqual(mockEntityDataWithNestingResponse);
});
it('getFormattedEntityData should return empty array in case entityType is given other than from type SummaryEntityType', () => {
const resultFormattedData = getFormattedEntityData(
'otherType' as SummaryEntityType,
mockEntityDataWithNesting
);
expect(resultFormattedData).toEqual([]);
});
it('getFormattedEntityData should not throw error if entityDetails sent does not have fields present', () => {
const resultFormattedData = getFormattedEntityData(
SummaryEntityType.COLUMN,
[{}] as Column[]
);
expect(resultFormattedData).toEqual(mockInvalidDataResponse);
});
});

View File

@ -0,0 +1,111 @@
/*
* 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 { Space, Typography } from 'antd';
import { isEmpty } from 'lodash';
import React from 'react';
import { Link } from 'react-router-dom';
import { BasicEntityInfo } from '../components/Explore/EntitySummaryPanel/SummaryList/SummaryList.interface';
import { SummaryEntityType } from '../enums/EntitySummary.enum';
import { Chart } from '../generated/entity/data/chart';
import { MlFeature } from '../generated/entity/data/mlmodel';
import { Task } from '../generated/entity/data/pipeline';
import { Column } from '../generated/entity/data/table';
import { Field } from '../generated/entity/data/topic';
import { getEntityName } from './CommonUtils';
import SVGIcons from './SvgUtils';
const { Text } = Typography;
export const getFormattedEntityData = (
entityType: SummaryEntityType,
entityInfo?: Column[] | Field[] | Chart[] | Task[] | MlFeature[]
): BasicEntityInfo[] => {
if (isEmpty(entityInfo)) {
return [];
}
switch (entityType) {
case SummaryEntityType.COLUMN: {
return (entityInfo as Column[]).map((column) => ({
name: column.name,
title: <Text className="entity-title">{column.name}</Text>,
type: column.dataType,
tags: column.tags,
description: column.description,
constraint: column.constraint,
children: getFormattedEntityData(
SummaryEntityType.COLUMN,
column.children
),
}));
}
case SummaryEntityType.CHART: {
return (entityInfo as Chart[]).map((chart) => ({
name: chart.name,
title: (
<Link target="_blank" to={{ pathname: chart.chartUrl }}>
<Space className="m-b-xs">
<Text className="entity-title link">{getEntityName(chart)}</Text>
<SVGIcons alt="external-link" icon="external-link" width="12px" />
</Space>
</Link>
),
type: chart.chartType,
tags: chart.tags,
description: chart.description,
}));
}
case SummaryEntityType.TASK: {
return (entityInfo as Task[]).map((task) => ({
name: task.name,
title: (
<Link target="_blank" to={{ pathname: task.taskUrl }}>
<Space className="m-b-xs">
<Text className="entity-title link">{task.name}</Text>
<SVGIcons alt="external-link" icon="external-link" width="12px" />
</Space>
</Link>
),
type: task.taskType,
tags: task.tags,
description: task.description,
}));
}
case SummaryEntityType.MLFEATURE: {
return (entityInfo as MlFeature[]).map((feature) => ({
algorithm: feature.featureAlgorithm,
name: feature.name || '--',
title: <Text className="entity-title">{feature.name}</Text>,
type: feature.dataType,
tags: feature.tags,
description: feature.description,
}));
}
case SummaryEntityType.SCHEMAFIELD: {
return (entityInfo as Field[]).map((field) => ({
name: field.name,
title: <Text className="entity-title">{field.name}</Text>,
type: field.dataType,
description: field.description,
tags: field.tags,
children: getFormattedEntityData(
SummaryEntityType.SCHEMAFIELD,
field.children
),
}));
}
default: {
return [];
}
}
};

View File

@ -89,5 +89,6 @@ export const getConfigObject = (
'Retention Size': topicDetails.retentionSize,
'CleanUp Policies': topicDetails.cleanupPolicies,
'Max Message Size': topicDetails.maximumMessageSize,
'Schema Type': topicDetails.messageSchema?.schemaType,
};
};

View File

@ -0,0 +1,241 @@
/*
* 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 { Typography } from 'antd';
import React from 'react';
import {
Column,
DataType,
LabelType,
State,
TagSource,
} from '../../generated/entity/data/table';
import { DataTypeTopic, Field } from '../../generated/entity/data/topic';
const { Text } = Typography;
export const mockEntityDataWithoutNesting: Column[] = [
{
name: 'api_client_id',
dataType: DataType.Numeric,
dataTypeDisplay: 'numeric',
description:
'ID of the API client that called the Shopify API. For example, the ID for the online store is 580111.',
fullyQualifiedName:
'sample_data.ecommerce_db.shopify."dim.api/client".api_client_id',
tags: [
{
tagFQN: 'PersonalData.SpecialCategory',
description:
'GDPR special category data is personal information of data subjects that is especially sensitive.',
source: TagSource.Tag,
labelType: LabelType.Manual,
state: State.Confirmed,
},
],
ordinalPosition: 1,
},
{
name: 'title',
dataType: DataType.Varchar,
dataLength: 100,
dataTypeDisplay: 'varchar',
description:
'Full name of the app or channel. For example, Point of Sale, Online Store.',
fullyQualifiedName:
'sample_data.ecommerce_db.shopify."dim.api/client".title',
tags: [],
ordinalPosition: 2,
},
];
export const mockEntityDataWithoutNestingResponse = [
{
children: [],
constraint: undefined,
description:
'ID of the API client that called the Shopify API. For example, the ID for the online store is 580111.',
name: 'api_client_id',
tags: [
{
tagFQN: 'PersonalData.SpecialCategory',
description:
'GDPR special category data is personal information of data subjects that is especially sensitive.',
source: 'Tag',
labelType: 'Manual',
state: 'Confirmed',
},
],
title: <Text className="entity-title">api_client_id</Text>,
type: 'NUMERIC',
},
{
children: [],
constraint: undefined,
description:
'Full name of the app or channel. For example, Point of Sale, Online Store.',
name: 'title',
tags: [],
title: <Text className="entity-title">title</Text>,
type: 'VARCHAR',
},
];
export const mockEntityDataWithNesting: Field[] = [
{
name: 'Customer',
dataType: DataTypeTopic.Record,
fullyQualifiedName: 'sample_kafka.customer_events.Customer',
tags: [],
children: [
{
name: 'id',
dataType: DataTypeTopic.String,
fullyQualifiedName: 'sample_kafka.customer_events.Customer.id',
tags: [],
},
{
name: 'first_name',
dataType: DataTypeTopic.String,
fullyQualifiedName: 'sample_kafka.customer_events.Customer.first_name',
tags: [],
},
{
name: 'last_name',
dataType: DataTypeTopic.String,
fullyQualifiedName: 'sample_kafka.customer_events.Customer.last_name',
tags: [],
},
{
name: 'email',
dataType: DataTypeTopic.String,
fullyQualifiedName: 'sample_kafka.customer_events.Customer.email',
tags: [],
},
{
name: 'address_line_1',
dataType: DataTypeTopic.String,
fullyQualifiedName:
'sample_kafka.customer_events.Customer.address_line_1',
tags: [],
},
{
name: 'address_line_2',
dataType: DataTypeTopic.String,
fullyQualifiedName:
'sample_kafka.customer_events.Customer.address_line_2',
tags: [],
},
{
name: 'post_code',
dataType: DataTypeTopic.String,
fullyQualifiedName: 'sample_kafka.customer_events.Customer.post_code',
tags: [],
},
{
name: 'country',
dataType: DataTypeTopic.String,
fullyQualifiedName: 'sample_kafka.customer_events.Customer.country',
tags: [],
},
],
},
];
export const mockEntityDataWithNestingResponse = [
{
children: [
{
children: [],
description: undefined,
name: 'id',
tags: [],
title: <Text className="entity-title">id</Text>,
type: 'STRING',
},
{
children: [],
description: undefined,
name: 'first_name',
tags: [],
title: <Text className="entity-title">first_name</Text>,
type: 'STRING',
},
{
children: [],
description: undefined,
name: 'last_name',
tags: [],
title: <Text className="entity-title">last_name</Text>,
type: 'STRING',
},
{
children: [],
description: undefined,
name: 'email',
tags: [],
title: <Text className="entity-title">email</Text>,
type: 'STRING',
},
{
children: [],
description: undefined,
name: 'address_line_1',
tags: [],
title: <Text className="entity-title">address_line_1</Text>,
type: 'STRING',
},
{
children: [],
description: undefined,
name: 'address_line_2',
tags: [],
title: <Text className="entity-title">address_line_2</Text>,
type: 'STRING',
},
{
children: [],
description: undefined,
name: 'post_code',
tags: [],
title: <Text className="entity-title">post_code</Text>,
type: 'STRING',
},
{
children: [],
description: undefined,
name: 'country',
tags: [],
title: <Text className="entity-title">country</Text>,
type: 'STRING',
},
],
description: undefined,
name: 'Customer',
tags: [],
title: <Text className="entity-title">Customer</Text>,
type: 'RECORD',
},
];
export const mockInvalidDataResponse = [
{
children: [],
constraints: undefined,
description: undefined,
name: undefined,
tags: undefined,
title: <Text className="entity-title" />,
type: undefined,
},
];