Feat(UI): Added EntitySummaryPanel in Explore page (#8591)

* Added EntitySummaryPanel in Explore page

* Worked on comments to optimise code for EntitySummaryPanel

* fixed faling unit test for explore component
This commit is contained in:
Aniket Katkar 2022-11-09 11:11:01 +05:30 committed by GitHub
parent e1806b50a5
commit 7c395a82d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 619 additions and 150 deletions

View File

@ -0,0 +1,87 @@
/*
* Copyright 2022 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Divider, Space, Typography } from 'antd';
import { toLower } from 'lodash';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { getTagValue } from '../../../utils/CommonUtils';
import SVGIcons from '../../../utils/SvgUtils';
import RichTextEditorPreviewer from '../../common/rich-text-editor/RichTextEditorPreviewer';
import TagsViewer from '../../tags-viewer/tags-viewer';
import { BasicColumnInfo, ColumnSummaryProps } from './ColumnSummary.interface';
const { Text, Paragraph } = Typography;
export default function ColumnSummary({ columns }: ColumnSummaryProps) {
const { t } = useTranslation();
const formattedColumnsData: BasicColumnInfo[] = useMemo(() => {
if (columns) {
return columns.map((column) => ({
name: column.name,
type: column.dataType,
tags: column.tags,
description: column.description,
}));
} else return [];
}, [columns]);
return (
<Space direction="vertical">
{columns &&
formattedColumnsData.map((column) => (
<React.Fragment key={column.name}>
<Space direction="vertical" size={0}>
<Text className="column-name">{column.name}</Text>
<Space className="text-xs" size={4}>
<Space size={4}>
<Text className="text-gray">{`${t('label.type')}:`}</Text>
<Text className="text-semi-bold">{toLower(column.type)}</Text>
</Space>
{column.tags?.length !== 0 && (
<>
<Divider type="vertical" />
<Space size={4}>
<SVGIcons
alt="icon-tag"
icon="icon-tag-grey"
width="12"
/>
<TagsViewer
sizeCap={-1}
tags={(column.tags || []).map((tag) =>
getTagValue(tag)
)}
/>
</Space>
</>
)}
</Space>
<Paragraph className="text-gray">
{column.description ? (
<RichTextEditorPreviewer
markdown={column.description || ''}
/>
) : (
t('label.no-description')
)}
</Paragraph>
</Space>
<Divider />
</React.Fragment>
))}
</Space>
);
}

View File

@ -0,0 +1,26 @@
/*
* Copyright 2022 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Column, DataType } from '../../../generated/entity/data/table';
import { TagLabel } from './../../../generated/type/tagLabel';
export interface ColumnSummaryProps {
columns: Column[];
}
export interface BasicColumnInfo {
name: string;
type: DataType;
tags?: TagLabel[];
description?: string;
}

View File

@ -0,0 +1,100 @@
/*
* Copyright 2022 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CloseOutlined } from '@ant-design/icons';
import { Col, Divider, Row, Space, Typography } from 'antd';
import classNames from 'classnames';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { TableType } from '../../../generated/entity/data/table';
import ColumnSummary from '../ColumnSummary/ColumnSummary.component';
import {
BasicTableInfo,
EntitySummaryPanelProps,
} from './EntitySummaryPanel.interface';
import './EntitySummaryPanel.style.less';
export default function EntitySummaryPanel({
entityDetails,
handleClosePanel,
overallSummery,
showPanel,
}: EntitySummaryPanelProps) {
const { t } = useTranslation();
const { tableType, columns, tableQueries } = entityDetails;
const basicTableInfo: BasicTableInfo = {
Type: tableType || TableType.Regular,
Queries: tableQueries?.length ? `${tableQueries?.length}` : '-',
Columns: columns?.length ? `${columns?.length}` : '-',
};
return (
<div
className={classNames(
'summary-panel-container',
showPanel ? 'show-panel' : ''
)}>
<Space
className={classNames('basic-info-container m-md')}
direction="vertical">
<Typography.Title level={5}>{entityDetails.name}</Typography.Title>
<Space className={classNames('w-full')} direction="vertical">
{Object.keys(basicTableInfo).map((fieldName) => (
<Row gutter={16} key={fieldName}>
<Col className="text-gray" span={10}>
{fieldName}
</Col>
<Col span={12}>
{basicTableInfo[fieldName as keyof BasicTableInfo]}
</Col>
</Row>
))}
</Space>
</Space>
<Divider className="m-0" />
<Space className={classNames('m-md')} direction="vertical">
<Typography.Text className="section-header">
{t('label.profiler-amp-data-quality')}
</Typography.Text>
<Row gutter={[16, 16]}>
{overallSummery.map((field) => (
<Col key={field.title} span={10}>
<Space direction="vertical" size={6}>
<Typography.Text className="text-gray">
{field.title}
</Typography.Text>
<Typography.Text
className={classNames(
'tw-text-2xl tw-font-semibold',
field.className
)}>
{field.value}
</Typography.Text>
</Space>
</Col>
))}
</Row>
</Space>
<Divider className="m-0" />
<Space className={classNames('m-md')} direction="vertical">
<Typography.Text className="section-header">
{t('label.schema')}
</Typography.Text>
<ColumnSummary columns={columns} />
</Space>
<CloseOutlined className="close-icon" onClick={handleClosePanel} />
</div>
);
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2022 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Table, TableType } from '../../../generated/entity/data/table';
import { OverallTableSummeryType } from '../../TableProfiler/TableProfiler.interface';
export interface EntitySummaryPanelProps {
entityDetails: Table;
handleClosePanel: () => void;
overallSummery: OverallTableSummeryType[];
showPanel: boolean;
}
export interface BasicTableInfo {
Type: TableType | string;
Queries: string;
Columns: string;
}

View File

@ -0,0 +1,84 @@
/*
* Copyright 2022 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@white: #ffffff;
@summary-panel-offset: 118px; // (topnav height) 64px + (margin) 16px + (tabs height) 38px
@label-color: #37352f99;
@succesColor: #28a745;
@failedColor: #cb2431;
@abortedColor: #efae2f;
.summary-panel-container {
width: 380px;
display: none;
background-color: @white;
height: calc(100vh - @summary-panel-offset);
}
.show-panel {
display: block;
position: fixed;
top: @summary-panel-offset;
right: 0px;
z-index: 1000;
box-shadow: -2px 2px 4px rgba(0, 0, 0, 0.12);
font-size: 14px;
overflow-y: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
.basic-info-container {
width: calc(100% - 32px);
}
.text-gray {
color: @label-color;
}
.section-header {
font-size: 16px;
}
.success {
color: @succesColor;
}
.failed {
color: @failedColor;
}
.aborted {
color: @abortedColor;
}
.column-name {
font-weight: 500;
}
.ant-divider-horizontal {
margin: 8px 0px;
}
div.ant-typography {
margin-top: 16px;
margin-bottom: 0px;
}
.close-icon {
position: absolute;
top: 16px;
right: 16px;
}
}
.show-panel::-webkit-scrollbar {
display: none;
}

View File

@ -16,22 +16,42 @@ import {
faSortAmountUpAlt, faSortAmountUpAlt,
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Card, Tabs } from 'antd'; import { Card, Space, Tabs } from 'antd';
import { AxiosError } from 'axios';
import unique from 'fork-ts-checker-webpack-plugin/lib/utils/array/unique'; import unique from 'fork-ts-checker-webpack-plugin/lib/utils/array/unique';
import { isNil, isNumber, lowerCase, noop, omit, toUpper } from 'lodash'; import { isNil, isNumber, lowerCase, noop, omit, toUpper } from 'lodash';
import { EntityType } from 'Models'; import { EntityType } from 'Models';
import React, { Fragment, useEffect, useMemo, useRef } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { getTableDetailsByFQN } from '../../axiosAPIs/tableAPI';
import { getListTestCase } from '../../axiosAPIs/testAPI';
import FacetFilter from '../../components/common/facetfilter/FacetFilter'; import FacetFilter from '../../components/common/facetfilter/FacetFilter';
import SearchedData from '../../components/searched-data/SearchedData'; import SearchedData from '../../components/searched-data/SearchedData';
import { ENTITY_PATH } from '../../constants/constants'; import { API_RES_MAX_SIZE, ENTITY_PATH } from '../../constants/constants';
import { tabsInfo } from '../../constants/explore.constants'; import { tabsInfo } from '../../constants/explore.constants';
import { INITIAL_TEST_RESULT_SUMMARY } from '../../constants/profiler.constant';
import { TabSpecificField } from '../../enums/entity.enum';
import { SearchIndex } from '../../enums/search.enum'; import { SearchIndex } from '../../enums/search.enum';
import { getCountBadge } from '../../utils/CommonUtils'; import { Table } from '../../generated/entity/data/table';
import { Include } from '../../generated/type/include';
import {
formatNumberWithComma,
formTwoDigitNmber,
getCountBadge,
} from '../../utils/CommonUtils';
import { updateTestResults } from '../../utils/DataQualityAndProfilerUtils';
import { generateEntityLink } from '../../utils/TableUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import AdvancedSearch from '../AdvancedSearch/AdvancedSearch.component'; import AdvancedSearch from '../AdvancedSearch/AdvancedSearch.component';
import { FacetFilterProps } from '../common/facetfilter/facetFilter.interface'; import { FacetFilterProps } from '../common/facetfilter/facetFilter.interface';
import PageLayout, { leftPanelAntCardStyle } from '../containers/PageLayout'; import PageLayoutV1 from '../containers/PageLayoutV1';
import Loader from '../Loader/Loader'; import Loader from '../Loader/Loader';
import {
OverallTableSummeryType,
TableTestsType,
} from '../TableProfiler/TableProfiler.interface';
import EntitySummaryPanel from './EntitySummaryPanel/EntitySummaryPanel.component';
import { import {
ExploreProps, ExploreProps,
ExploreSearchIndex, ExploreSearchIndex,
@ -61,6 +81,17 @@ const Explore: React.FC<ExploreProps> = ({
}) => { }) => {
const isMounting = useRef(true); const isMounting = useRef(true);
const { tab } = useParams<{ tab: string }>(); const { tab } = useParams<{ tab: string }>();
const { t } = useTranslation();
const [showSummaryPanel, setShowSummaryPanel] = useState(false);
const [entityDetails, setEntityDetails] = useState<Table>();
const [tableTests, setTableTests] = useState<TableTestsType>({
tests: [],
results: INITIAL_TEST_RESULT_SUMMARY,
});
const handleClosePanel = () => {
setShowSummaryPanel(false);
};
// get entity active tab by URL params // get entity active tab by URL params
const defaultActiveTab = useMemo(() => { const defaultActiveTab = useMemo(() => {
@ -96,6 +127,99 @@ const Explore: React.FC<ExploreProps> = ({
} }
}; };
const overallSummery: OverallTableSummeryType[] = useMemo(() => {
return [
{
title: 'Row Count',
value: formatNumberWithComma(entityDetails?.profile?.rowCount ?? 0),
},
{
title: 'Column Count',
value: entityDetails?.profile?.columnCount ?? 0,
},
{
title: 'Table Sample %',
value: `${entityDetails?.profile?.profileSample ?? 100}%`,
},
{
title: 'Tests Passed',
value: formTwoDigitNmber(tableTests.results.success),
className: 'success',
},
{
title: 'Tests Aborted',
value: formTwoDigitNmber(tableTests.results.aborted),
className: 'aborted',
},
{
title: 'Tests Failed',
value: formTwoDigitNmber(tableTests.results.failed),
className: 'failed',
},
];
}, [entityDetails, tableTests]);
const fetchProfilerData = async (source: Table) => {
try {
const res = await getTableDetailsByFQN(
encodeURIComponent(source?.fullyQualifiedName || ''),
`${TabSpecificField.TABLE_PROFILE},${TabSpecificField.TABLE_QUERIES}`
);
const { profile, tableQueries } = res;
setEntityDetails((prev) => {
if (prev) {
return { ...prev, profile, tableQueries };
} else {
return {} as Table;
}
});
} catch {
showErrorToast(
t('message.entity-fetch-error', {
entity: `profile details for table ${source?.name || ''}`,
})
);
}
};
const fetchAllTests = async (source: Table) => {
try {
const { data } = await getListTestCase({
fields: 'testCaseResult,entityLink,testDefinition,testSuite',
entityLink: generateEntityLink(source?.fullyQualifiedName || ''),
includeAllTests: true,
limit: API_RES_MAX_SIZE,
include: Include.Deleted,
});
const tableTests: TableTestsType = {
tests: [],
results: { ...INITIAL_TEST_RESULT_SUMMARY },
};
data.forEach((test) => {
if (test.entityFQN === source?.fullyQualifiedName) {
tableTests.tests.push(test);
updateTestResults(
tableTests.results,
test.testCaseResult?.testCaseStatus || ''
);
return;
}
});
setTableTests(tableTests);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const handleSummaryPanelDisplay = (source: Table) => {
setShowSummaryPanel(true);
fetchAllTests(source);
fetchProfilerData(source);
setEntityDetails(source);
};
const handleFacetFilterClearFilter: FacetFilterProps['onClearFilter'] = ( const handleFacetFilterClearFilter: FacetFilterProps['onClearFilter'] = (
key key
) => onChangePostFilter(omit(postFilter, key)); ) => onChangePostFilter(omit(postFilter, key));
@ -103,114 +227,134 @@ const Explore: React.FC<ExploreProps> = ({
// alwyas Keep this useEffect at the end... // alwyas Keep this useEffect at the end...
useEffect(() => { useEffect(() => {
isMounting.current = false; isMounting.current = false;
const escapeKeyHandler = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
handleClosePanel();
}
};
document.addEventListener('keydown', escapeKeyHandler);
return () => {
document.removeEventListener('keydown', escapeKeyHandler);
};
}, []); }, []);
return ( return (
<Fragment> <PageLayoutV1
<PageLayout leftPanel={
leftPanel={ <div className="tw-h-full">
<div className="tw-h-full"> <Card data-testid="data-summary-container">
<Card <FacetFilter
data-testid="data-summary-container" aggregations={searchResults?.aggregations}
style={{ ...leftPanelAntCardStyle, marginTop: '16px' }}> filters={postFilter}
<FacetFilter showDeleted={showDeleted}
aggregations={searchResults?.aggregations} onChangeShowDeleted={onChangeShowDeleted}
filters={postFilter} onClearFilter={handleFacetFilterClearFilter}
showDeleted={showDeleted} onSelectHandler={handleFacetFilterChange}
onChangeShowDeleted={onChangeShowDeleted}
onClearFilter={handleFacetFilterClearFilter}
onSelectHandler={handleFacetFilterChange}
/>
</Card>
</div>
}>
<Tabs
defaultActiveKey={defaultActiveTab}
size="small"
tabBarExtraContent={
<div className="tw-flex">
<SortingDropDown
fieldList={tabsInfo[searchIndex].sortingFields}
handleFieldDropDown={onChangeSortValue}
sortField={sortValue}
/>
<div className="tw-flex">
{sortOrder === 'asc' ? (
<button
className="tw-mt-2"
onClick={() => onChangeSortOder('desc')}>
<FontAwesomeIcon
className="tw-text-base tw-text-primary"
data-testid="last-updated"
icon={faSortAmountUpAlt}
/>
</button>
) : (
<button
className="tw-mt-2"
onClick={() => onChangeSortOder('asc')}>
<FontAwesomeIcon
className="tw-text-base tw-text-primary"
data-testid="last-updated"
icon={faSortAmountDownAlt}
/>
</button>
)}
</div>
</div>
}
onChange={(tab) => {
tab && onChangeSearchIndex(tab as ExploreSearchIndex);
}}>
{Object.entries(tabsInfo).map(([tabSearchIndex, tabDetail]) => (
<Tabs.TabPane
key={tabSearchIndex}
tab={
<div data-testid={`${lowerCase(tabDetail.label)}-tab`}>
{tabDetail.label}
<span className="p-l-xs ">
{!isNil(tabCounts)
? getCountBadge(
tabCounts[tabSearchIndex as ExploreSearchIndex],
'',
tabSearchIndex === searchIndex
)
: getCountBadge()}
</span>
</div>
}
/> />
))} </Card>
</Tabs> </div>
<AdvancedSearch }>
jsonTree={advancedSearchJsonTree} <Tabs
searchIndex={searchIndex} defaultActiveKey={defaultActiveTab}
onChangeJsonTree={(nTree) => onChangeAdvancedSearchJsonTree(nTree)} size="small"
onChangeQueryFilter={(nQueryFilter) => tabBarExtraContent={
onChangeAdvancedSearchQueryFilter(nQueryFilter) <div className="tw-flex">
} <SortingDropDown
/> fieldList={tabsInfo[searchIndex].sortingFields}
{!loading ? ( handleFieldDropDown={onChangeSortValue}
<SearchedData sortField={sortValue}
isFilterSelected />
showResultCount
currentPage={page} <div className="tw-flex">
data={searchResults?.hits.hits ?? []} {sortOrder === 'asc' ? (
paginate={(value) => { <button
if (isNumber(value)) { className="tw-mt-2"
onChangePage(value); onClick={() => onChangeSortOder('desc')}>
} else if (!isNaN(Number.parseInt(value))) { <FontAwesomeIcon
onChangePage(Number.parseInt(value)); className="tw-text-base tw-text-primary"
} data-testid="last-updated"
}} icon={faSortAmountUpAlt}
totalValue={searchResults?.hits.total.value ?? 0} />
</button>
) : (
<button
className="tw-mt-2"
onClick={() => onChangeSortOder('asc')}>
<FontAwesomeIcon
className="tw-text-base tw-text-primary"
data-testid="last-updated"
icon={faSortAmountDownAlt}
/>
</button>
)}
</div>
</div>
}
onChange={(tab) => {
tab && onChangeSearchIndex(tab as ExploreSearchIndex);
}}>
{Object.entries(tabsInfo).map(([tabSearchIndex, tabDetail]) => (
<Tabs.TabPane
key={tabSearchIndex}
tab={
<div data-testid={`${lowerCase(tabDetail.label)}-tab`}>
{tabDetail.label}
<span className="p-l-xs ">
{!isNil(tabCounts)
? getCountBadge(
tabCounts[tabSearchIndex as ExploreSearchIndex],
'',
tabSearchIndex === searchIndex
)
: getCountBadge()}
</span>
</div>
}
/> />
) : ( ))}
<Loader /> </Tabs>
)} <Space>
</PageLayout> <div
</Fragment> style={{
marginRight: showSummaryPanel ? '380px' : '',
}}>
<AdvancedSearch
jsonTree={advancedSearchJsonTree}
searchIndex={searchIndex}
onChangeJsonTree={(nTree) => onChangeAdvancedSearchJsonTree(nTree)}
onChangeQueryFilter={(nQueryFilter) =>
onChangeAdvancedSearchQueryFilter(nQueryFilter)
}
/>
{!loading ? (
<SearchedData
isFilterSelected
showResultCount
currentPage={page}
data={searchResults?.hits.hits ?? []}
handleSummaryPanelDisplay={handleSummaryPanelDisplay}
paginate={(value) => {
if (isNumber(value)) {
onChangePage(value);
} else if (!isNaN(Number.parseInt(value))) {
onChangePage(Number.parseInt(value));
}
}}
totalValue={searchResults?.hits.total.value ?? 0}
/>
) : (
<Loader />
)}
</div>
<EntitySummaryPanel
entityDetails={entityDetails || ({} as Table)}
handleClosePanel={handleClosePanel}
overallSummery={overallSummery}
showPanel={showSummaryPanel}
/>
</Space>
</PageLayoutV1>
); );
}; };

View File

@ -59,27 +59,6 @@ jest.mock('../../components/searched-data/SearchedData', () => {
)); ));
}); });
jest.mock(
'../containers/PageLayout',
() =>
({
children,
leftPanel,
rightPanel,
}: {
children: React.ReactNode;
rightPanel: React.ReactNode;
leftPanel: React.ReactNode;
}) =>
(
<div data-testid="PageLayout">
<div data-testid="left-panel-content">{leftPanel}</div>
<div data-testid="right-panel-content">{rightPanel}</div>
{children}
</div>
)
);
const mockFunction = jest.fn(); const mockFunction = jest.fn();
describe('Test Explore component', () => { describe('Test Explore component', () => {
@ -110,12 +89,10 @@ describe('Test Explore component', () => {
wrapper: MemoryRouter, wrapper: MemoryRouter,
} }
); );
const pageContainer = await findByTestId(container, 'PageLayout');
const searchData = await findByTestId(container, 'search-data'); const searchData = await findByTestId(container, 'search-data');
const wrappedContent = await findByTestId(container, 'wrapped-content'); const wrappedContent = await findByTestId(container, 'wrapped-content');
const tabs = await findAllByTestId(container, /tab/i); const tabs = await findAllByTestId(container, /tab/i);
expect(pageContainer).toBeInTheDocument();
expect(searchData).toBeInTheDocument(); expect(searchData).toBeInTheDocument();
expect(wrappedContent).toBeInTheDocument(); expect(wrappedContent).toBeInTheDocument();
expect(tabs.length).toBe(5); expect(tabs.length).toBe(5);

View File

@ -38,10 +38,13 @@ jest.mock('../table-data-card/TableDataCardBody', () => {
return jest.fn().mockReturnValue(<p>TableDataCardBody</p>); return jest.fn().mockReturnValue(<p>TableDataCardBody</p>);
}); });
const mockHandleSummaryPanelDisplay = jest.fn();
describe('Test TableDataCard Component', () => { describe('Test TableDataCard Component', () => {
it('Component should render', () => { it('Component should render', () => {
const { getByTestId } = render( const { getByTestId } = render(
<TableDataCardV2 <TableDataCardV2
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
id="1" id="1"
searchIndex={SearchIndex.TABLE} searchIndex={SearchIndex.TABLE}
source={{ source={{
@ -58,6 +61,7 @@ describe('Test TableDataCard Component', () => {
it('Component should render for deleted', () => { it('Component should render for deleted', () => {
const { getByTestId } = render( const { getByTestId } = render(
<TableDataCardV2 <TableDataCardV2
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
id="1" id="1"
searchIndex={SearchIndex.TABLE} searchIndex={SearchIndex.TABLE}
source={{ source={{

View File

@ -24,6 +24,7 @@ import { EntityType, FqnPart } from '../../../enums/entity.enum';
import { SearchIndex } from '../../../enums/search.enum'; import { SearchIndex } from '../../../enums/search.enum';
import { CurrentTourPageType } from '../../../enums/tour.enum'; import { CurrentTourPageType } from '../../../enums/tour.enum';
import { OwnerType } from '../../../enums/user.enum'; import { OwnerType } from '../../../enums/user.enum';
import { Table } from '../../../generated/entity/data/table';
import { EntityReference } from '../../../generated/entity/type'; import { EntityReference } from '../../../generated/entity/type';
import { import {
getEntityId, getEntityId,
@ -48,6 +49,7 @@ export interface TableDataCardPropsV2 {
value: number; value: number;
}[]; }[];
searchIndex: SearchIndex | EntityType; searchIndex: SearchIndex | EntityType;
handleSummaryPanelDisplay: (source: Table) => void;
} }
const TableDataCardV2: React.FC<TableDataCardPropsV2> = ({ const TableDataCardV2: React.FC<TableDataCardPropsV2> = ({
@ -55,6 +57,7 @@ const TableDataCardV2: React.FC<TableDataCardPropsV2> = ({
source, source,
matches, matches,
searchIndex, searchIndex,
handleSummaryPanelDisplay,
}) => { }) => {
const location = useLocation(); const location = useLocation();
@ -141,7 +144,8 @@ const TableDataCardV2: React.FC<TableDataCardPropsV2> = ({
return ( return (
<div <div
className="tw-bg-white tw-p-3 tw-border tw-border-main tw-rounded-md" className="tw-bg-white tw-p-3 tw-border tw-border-main tw-rounded-md"
data-testid="table-data-card"> data-testid="table-data-card"
onClick={() => handleSummaryPanelDisplay(source as Table)}>
<div> <div>
{'databaseSchema' in source && 'database' in source && ( {'databaseSchema' in source && 'database' in source && (
<span <span

View File

@ -11,11 +11,11 @@
* limitations under the License. * limitations under the License.
*/ */
import { isNil, isString } from 'lodash'; import { isNil } from 'lodash';
import { ExtraInfo } from 'Models'; import { ExtraInfo } from 'Models';
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import { TagLabel } from '../../../generated/type/tagLabel'; import { TagLabel } from '../../../generated/type/tagLabel';
import { getTagValue } from '../../../utils/CommonUtils';
import SVGIcons from '../../../utils/SvgUtils'; import SVGIcons from '../../../utils/SvgUtils';
import TagsViewer from '../../tags-viewer/tags-viewer'; import TagsViewer from '../../tags-viewer/tags-viewer';
import EntitySummaryDetails from '../EntitySummaryDetails/EntitySummaryDetails'; import EntitySummaryDetails from '../EntitySummaryDetails/EntitySummaryDetails';
@ -32,21 +32,6 @@ const TableDataCardBody: FunctionComponent<Props> = ({
extraInfo, extraInfo,
tags, tags,
}: Props) => { }: Props) => {
const getTagValue = (tag: string | TagLabel): string | TagLabel => {
if (isString(tag)) {
return tag.startsWith(`Tier${FQN_SEPARATOR_CHAR}Tier`)
? tag.split(FQN_SEPARATOR_CHAR)[1]
: tag;
} else {
return {
...tag,
tagFQN: tag.tagFQN.startsWith(`Tier${FQN_SEPARATOR_CHAR}Tier`)
? tag.tagFQN.split(FQN_SEPARATOR_CHAR)[1]
: tag.tagFQN,
};
}
};
return ( return (
<div data-testid="table-body"> <div data-testid="table-body">
<div className="tw-mb-4 tw-flex tw-items-center tw-flex-wrap tw-text-xs"> <div className="tw-mb-4 tw-flex tw-items-center tw-flex-wrap tw-text-xs">

View File

@ -12,6 +12,7 @@
*/ */
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Table } from '../../generated/entity/data/table';
import { EntityReference } from '../../generated/entity/type'; import { EntityReference } from '../../generated/entity/type';
import { TagLabel } from '../../generated/type/tagLabel'; import { TagLabel } from '../../generated/type/tagLabel';
import { import {
@ -67,4 +68,5 @@ export interface SearchedDataProps {
showOnboardingTemplate?: boolean; showOnboardingTemplate?: boolean;
showOnlyChildren?: boolean; showOnlyChildren?: boolean;
isFilterSelected: boolean; isFilterSelected: boolean;
handleSummaryPanelDisplay: (source: Table) => void;
} }

View File

@ -64,6 +64,7 @@ const mockData: SearchedDataProps['data'] = [
]; ];
const mockPaginate = jest.fn(); const mockPaginate = jest.fn();
const mockHandleSummaryPanelDisplay = jest.fn();
jest.mock('../common/table-data-card/TableDataCard', () => { jest.mock('../common/table-data-card/TableDataCard', () => {
return jest return jest
@ -90,6 +91,7 @@ describe('Test SearchedData Component', () => {
isFilterSelected isFilterSelected
currentPage={0} currentPage={0}
data={mockData} data={mockData}
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
paginate={mockPaginate} paginate={mockPaginate}
totalValue={10} totalValue={10}
/>, />,
@ -109,6 +111,7 @@ describe('Test SearchedData Component', () => {
isFilterSelected isFilterSelected
currentPage={0} currentPage={0}
data={mockData} data={mockData}
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
paginate={mockPaginate} paginate={mockPaginate}
totalValue={10} totalValue={10}
/>, />,
@ -128,6 +131,7 @@ describe('Test SearchedData Component', () => {
isFilterSelected isFilterSelected
currentPage={0} currentPage={0}
data={mockData} data={mockData}
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
paginate={mockPaginate} paginate={mockPaginate}
totalValue={10}> totalValue={10}>
<p>hello world</p> <p>hello world</p>
@ -146,6 +150,7 @@ describe('Test SearchedData Component', () => {
isFilterSelected isFilterSelected
currentPage={0} currentPage={0}
data={mockData} data={mockData}
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
paginate={mockPaginate} paginate={mockPaginate}
totalValue={11}> totalValue={11}>
<p>hello world</p> <p>hello world</p>
@ -165,6 +170,7 @@ describe('Test SearchedData Component', () => {
showOnboardingTemplate showOnboardingTemplate
currentPage={0} currentPage={0}
data={[]} data={[]}
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
paginate={mockPaginate} paginate={mockPaginate}
totalValue={0} totalValue={0}
/>, />,
@ -182,6 +188,7 @@ describe('Test SearchedData Component', () => {
isFilterSelected isFilterSelected
currentPage={0} currentPage={0}
data={[]} data={[]}
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
paginate={mockPaginate} paginate={mockPaginate}
totalValue={0} totalValue={0}
/>, />,

View File

@ -44,6 +44,7 @@ const SearchedData: React.FC<SearchedDataProps> = ({
totalValue, totalValue,
isFilterSelected, isFilterSelected,
searchText, searchText,
handleSummaryPanelDisplay,
}) => { }) => {
const highlightSearchResult = () => { const highlightSearchResult = () => {
return data.map(({ _source: table, highlight, _index }, index) => { return data.map(({ _source: table, highlight, _index }, index) => {
@ -89,6 +90,7 @@ const SearchedData: React.FC<SearchedDataProps> = ({
return ( return (
<div className="tw-mb-3" key={index}> <div className="tw-mb-3" key={index}>
<TableDataCardV2 <TableDataCardV2
handleSummaryPanelDisplay={handleSummaryPanelDisplay}
id={`tabledatacard${index}`} id={`tabledatacard${index}`}
matches={matches} matches={matches}
searchIndex={_index} searchIndex={_index}

View File

@ -207,7 +207,9 @@
"select-resource": "Select Resources", "select-resource": "Select Resources",
"select-rule-effect": "Select Rule Effect", "select-rule-effect": "Select Rule Effect",
"field-required": "{{field}} is required", "field-required": "{{field}} is required",
"field-required-plural": "{{field}} are required" "field-required-plural": "{{field}} are required",
"profiler-amp-data-quality": "Profiler & Data Quality",
"schema": "Schema"
}, },
"message": { "message": {
"service-email-required": "Service account Email is required", "service-email-required": "Service account Email is required",

View File

@ -24,6 +24,7 @@ import {
isEqual, isEqual,
isNil, isNil,
isNull, isNull,
isString,
isUndefined, isUndefined,
uniqueId, uniqueId,
} from 'lodash'; } from 'lodash';
@ -80,6 +81,7 @@ import { Role } from '../generated/entity/teams/role';
import { Team } from '../generated/entity/teams/team'; import { Team } from '../generated/entity/teams/team';
import { EntityReference, User } from '../generated/entity/teams/user'; import { EntityReference, User } from '../generated/entity/teams/user';
import { Paging } from '../generated/type/paging'; import { Paging } from '../generated/type/paging';
import { TagLabel } from '../generated/type/tagLabel';
import { ServicesType } from '../interface/service.interface'; import { ServicesType } from '../interface/service.interface';
import jsonData from '../jsons/en'; import jsonData from '../jsons/en';
import { getEntityFeedLink, getTitleCase } from './EntityUtils'; import { getEntityFeedLink, getTitleCase } from './EntityUtils';
@ -1010,3 +1012,18 @@ export const getTierFromEntityInfo = (entity: FormattedTableData) => {
getTierFromSearchTableTags((entity.tags || []).map((tag) => tag.tagFQN)) getTierFromSearchTableTags((entity.tags || []).map((tag) => tag.tagFQN))
)?.split(FQN_SEPARATOR_CHAR)[1]; )?.split(FQN_SEPARATOR_CHAR)[1];
}; };
export const getTagValue = (tag: string | TagLabel): string | TagLabel => {
if (isString(tag)) {
return tag.startsWith(`Tier${FQN_SEPARATOR_CHAR}Tier`)
? tag.split(FQN_SEPARATOR_CHAR)[1]
: tag;
} else {
return {
...tag,
tagFQN: tag.tagFQN.startsWith(`Tier${FQN_SEPARATOR_CHAR}Tier`)
? tag.tagFQN.split(FQN_SEPARATOR_CHAR)[1]
: tag.tagFQN,
};
}
};