mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-27 08:44:49 +00:00
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:
parent
e1806b50a5
commit
7c395a82d4
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -16,22 +16,42 @@ import {
|
||||
faSortAmountUpAlt,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
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 { isNil, isNumber, lowerCase, noop, omit, toUpper } from 'lodash';
|
||||
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 { getTableDetailsByFQN } from '../../axiosAPIs/tableAPI';
|
||||
import { getListTestCase } from '../../axiosAPIs/testAPI';
|
||||
import FacetFilter from '../../components/common/facetfilter/FacetFilter';
|
||||
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 { INITIAL_TEST_RESULT_SUMMARY } from '../../constants/profiler.constant';
|
||||
import { TabSpecificField } from '../../enums/entity.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 { FacetFilterProps } from '../common/facetfilter/facetFilter.interface';
|
||||
import PageLayout, { leftPanelAntCardStyle } from '../containers/PageLayout';
|
||||
import PageLayoutV1 from '../containers/PageLayoutV1';
|
||||
import Loader from '../Loader/Loader';
|
||||
import {
|
||||
OverallTableSummeryType,
|
||||
TableTestsType,
|
||||
} from '../TableProfiler/TableProfiler.interface';
|
||||
import EntitySummaryPanel from './EntitySummaryPanel/EntitySummaryPanel.component';
|
||||
import {
|
||||
ExploreProps,
|
||||
ExploreSearchIndex,
|
||||
@ -61,6 +81,17 @@ const Explore: React.FC<ExploreProps> = ({
|
||||
}) => {
|
||||
const isMounting = useRef(true);
|
||||
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
|
||||
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'] = (
|
||||
key
|
||||
) => onChangePostFilter(omit(postFilter, key));
|
||||
@ -103,114 +227,134 @@ const Explore: React.FC<ExploreProps> = ({
|
||||
// alwyas Keep this useEffect at the end...
|
||||
useEffect(() => {
|
||||
isMounting.current = false;
|
||||
const escapeKeyHandler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleClosePanel();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', escapeKeyHandler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', escapeKeyHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PageLayout
|
||||
leftPanel={
|
||||
<div className="tw-h-full">
|
||||
<Card
|
||||
data-testid="data-summary-container"
|
||||
style={{ ...leftPanelAntCardStyle, marginTop: '16px' }}>
|
||||
<FacetFilter
|
||||
aggregations={searchResults?.aggregations}
|
||||
filters={postFilter}
|
||||
showDeleted={showDeleted}
|
||||
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>
|
||||
}
|
||||
<PageLayoutV1
|
||||
leftPanel={
|
||||
<div className="tw-h-full">
|
||||
<Card data-testid="data-summary-container">
|
||||
<FacetFilter
|
||||
aggregations={searchResults?.aggregations}
|
||||
filters={postFilter}
|
||||
showDeleted={showDeleted}
|
||||
onChangeShowDeleted={onChangeShowDeleted}
|
||||
onClearFilter={handleFacetFilterClearFilter}
|
||||
onSelectHandler={handleFacetFilterChange}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
<AdvancedSearch
|
||||
jsonTree={advancedSearchJsonTree}
|
||||
searchIndex={searchIndex}
|
||||
onChangeJsonTree={(nTree) => onChangeAdvancedSearchJsonTree(nTree)}
|
||||
onChangeQueryFilter={(nQueryFilter) =>
|
||||
onChangeAdvancedSearchQueryFilter(nQueryFilter)
|
||||
}
|
||||
/>
|
||||
{!loading ? (
|
||||
<SearchedData
|
||||
isFilterSelected
|
||||
showResultCount
|
||||
currentPage={page}
|
||||
data={searchResults?.hits.hits ?? []}
|
||||
paginate={(value) => {
|
||||
if (isNumber(value)) {
|
||||
onChangePage(value);
|
||||
} else if (!isNaN(Number.parseInt(value))) {
|
||||
onChangePage(Number.parseInt(value));
|
||||
}
|
||||
}}
|
||||
totalValue={searchResults?.hits.total.value ?? 0}
|
||||
</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>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</PageLayout>
|
||||
</Fragment>
|
||||
))}
|
||||
</Tabs>
|
||||
<Space>
|
||||
<div
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
describe('Test Explore component', () => {
|
||||
@ -110,12 +89,10 @@ describe('Test Explore component', () => {
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
);
|
||||
const pageContainer = await findByTestId(container, 'PageLayout');
|
||||
const searchData = await findByTestId(container, 'search-data');
|
||||
const wrappedContent = await findByTestId(container, 'wrapped-content');
|
||||
const tabs = await findAllByTestId(container, /tab/i);
|
||||
|
||||
expect(pageContainer).toBeInTheDocument();
|
||||
expect(searchData).toBeInTheDocument();
|
||||
expect(wrappedContent).toBeInTheDocument();
|
||||
expect(tabs.length).toBe(5);
|
||||
|
||||
@ -38,10 +38,13 @@ jest.mock('../table-data-card/TableDataCardBody', () => {
|
||||
return jest.fn().mockReturnValue(<p>TableDataCardBody</p>);
|
||||
});
|
||||
|
||||
const mockHandleSummaryPanelDisplay = jest.fn();
|
||||
|
||||
describe('Test TableDataCard Component', () => {
|
||||
it('Component should render', () => {
|
||||
const { getByTestId } = render(
|
||||
<TableDataCardV2
|
||||
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
|
||||
id="1"
|
||||
searchIndex={SearchIndex.TABLE}
|
||||
source={{
|
||||
@ -58,6 +61,7 @@ describe('Test TableDataCard Component', () => {
|
||||
it('Component should render for deleted', () => {
|
||||
const { getByTestId } = render(
|
||||
<TableDataCardV2
|
||||
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
|
||||
id="1"
|
||||
searchIndex={SearchIndex.TABLE}
|
||||
source={{
|
||||
|
||||
@ -24,6 +24,7 @@ import { EntityType, FqnPart } from '../../../enums/entity.enum';
|
||||
import { SearchIndex } from '../../../enums/search.enum';
|
||||
import { CurrentTourPageType } from '../../../enums/tour.enum';
|
||||
import { OwnerType } from '../../../enums/user.enum';
|
||||
import { Table } from '../../../generated/entity/data/table';
|
||||
import { EntityReference } from '../../../generated/entity/type';
|
||||
import {
|
||||
getEntityId,
|
||||
@ -48,6 +49,7 @@ export interface TableDataCardPropsV2 {
|
||||
value: number;
|
||||
}[];
|
||||
searchIndex: SearchIndex | EntityType;
|
||||
handleSummaryPanelDisplay: (source: Table) => void;
|
||||
}
|
||||
|
||||
const TableDataCardV2: React.FC<TableDataCardPropsV2> = ({
|
||||
@ -55,6 +57,7 @@ const TableDataCardV2: React.FC<TableDataCardPropsV2> = ({
|
||||
source,
|
||||
matches,
|
||||
searchIndex,
|
||||
handleSummaryPanelDisplay,
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
|
||||
@ -141,7 +144,8 @@ const TableDataCardV2: React.FC<TableDataCardPropsV2> = ({
|
||||
return (
|
||||
<div
|
||||
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>
|
||||
{'databaseSchema' in source && 'database' in source && (
|
||||
<span
|
||||
|
||||
@ -11,11 +11,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isNil, isString } from 'lodash';
|
||||
import { isNil } from 'lodash';
|
||||
import { ExtraInfo } from 'Models';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
|
||||
import { TagLabel } from '../../../generated/type/tagLabel';
|
||||
import { getTagValue } from '../../../utils/CommonUtils';
|
||||
import SVGIcons from '../../../utils/SvgUtils';
|
||||
import TagsViewer from '../../tags-viewer/tags-viewer';
|
||||
import EntitySummaryDetails from '../EntitySummaryDetails/EntitySummaryDetails';
|
||||
@ -32,21 +32,6 @@ const TableDataCardBody: FunctionComponent<Props> = ({
|
||||
extraInfo,
|
||||
tags,
|
||||
}: 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 (
|
||||
<div data-testid="table-body">
|
||||
<div className="tw-mb-4 tw-flex tw-items-center tw-flex-wrap tw-text-xs">
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
*/
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { Table } from '../../generated/entity/data/table';
|
||||
import { EntityReference } from '../../generated/entity/type';
|
||||
import { TagLabel } from '../../generated/type/tagLabel';
|
||||
import {
|
||||
@ -67,4 +68,5 @@ export interface SearchedDataProps {
|
||||
showOnboardingTemplate?: boolean;
|
||||
showOnlyChildren?: boolean;
|
||||
isFilterSelected: boolean;
|
||||
handleSummaryPanelDisplay: (source: Table) => void;
|
||||
}
|
||||
|
||||
@ -64,6 +64,7 @@ const mockData: SearchedDataProps['data'] = [
|
||||
];
|
||||
|
||||
const mockPaginate = jest.fn();
|
||||
const mockHandleSummaryPanelDisplay = jest.fn();
|
||||
|
||||
jest.mock('../common/table-data-card/TableDataCard', () => {
|
||||
return jest
|
||||
@ -90,6 +91,7 @@ describe('Test SearchedData Component', () => {
|
||||
isFilterSelected
|
||||
currentPage={0}
|
||||
data={mockData}
|
||||
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
|
||||
paginate={mockPaginate}
|
||||
totalValue={10}
|
||||
/>,
|
||||
@ -109,6 +111,7 @@ describe('Test SearchedData Component', () => {
|
||||
isFilterSelected
|
||||
currentPage={0}
|
||||
data={mockData}
|
||||
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
|
||||
paginate={mockPaginate}
|
||||
totalValue={10}
|
||||
/>,
|
||||
@ -128,6 +131,7 @@ describe('Test SearchedData Component', () => {
|
||||
isFilterSelected
|
||||
currentPage={0}
|
||||
data={mockData}
|
||||
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
|
||||
paginate={mockPaginate}
|
||||
totalValue={10}>
|
||||
<p>hello world</p>
|
||||
@ -146,6 +150,7 @@ describe('Test SearchedData Component', () => {
|
||||
isFilterSelected
|
||||
currentPage={0}
|
||||
data={mockData}
|
||||
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
|
||||
paginate={mockPaginate}
|
||||
totalValue={11}>
|
||||
<p>hello world</p>
|
||||
@ -165,6 +170,7 @@ describe('Test SearchedData Component', () => {
|
||||
showOnboardingTemplate
|
||||
currentPage={0}
|
||||
data={[]}
|
||||
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
|
||||
paginate={mockPaginate}
|
||||
totalValue={0}
|
||||
/>,
|
||||
@ -182,6 +188,7 @@ describe('Test SearchedData Component', () => {
|
||||
isFilterSelected
|
||||
currentPage={0}
|
||||
data={[]}
|
||||
handleSummaryPanelDisplay={mockHandleSummaryPanelDisplay}
|
||||
paginate={mockPaginate}
|
||||
totalValue={0}
|
||||
/>,
|
||||
|
||||
@ -44,6 +44,7 @@ const SearchedData: React.FC<SearchedDataProps> = ({
|
||||
totalValue,
|
||||
isFilterSelected,
|
||||
searchText,
|
||||
handleSummaryPanelDisplay,
|
||||
}) => {
|
||||
const highlightSearchResult = () => {
|
||||
return data.map(({ _source: table, highlight, _index }, index) => {
|
||||
@ -89,6 +90,7 @@ const SearchedData: React.FC<SearchedDataProps> = ({
|
||||
return (
|
||||
<div className="tw-mb-3" key={index}>
|
||||
<TableDataCardV2
|
||||
handleSummaryPanelDisplay={handleSummaryPanelDisplay}
|
||||
id={`tabledatacard${index}`}
|
||||
matches={matches}
|
||||
searchIndex={_index}
|
||||
|
||||
@ -207,7 +207,9 @@
|
||||
"select-resource": "Select Resources",
|
||||
"select-rule-effect": "Select Rule Effect",
|
||||
"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": {
|
||||
"service-email-required": "Service account Email is required",
|
||||
|
||||
@ -24,6 +24,7 @@ import {
|
||||
isEqual,
|
||||
isNil,
|
||||
isNull,
|
||||
isString,
|
||||
isUndefined,
|
||||
uniqueId,
|
||||
} from 'lodash';
|
||||
@ -80,6 +81,7 @@ import { Role } from '../generated/entity/teams/role';
|
||||
import { Team } from '../generated/entity/teams/team';
|
||||
import { EntityReference, User } from '../generated/entity/teams/user';
|
||||
import { Paging } from '../generated/type/paging';
|
||||
import { TagLabel } from '../generated/type/tagLabel';
|
||||
import { ServicesType } from '../interface/service.interface';
|
||||
import jsonData from '../jsons/en';
|
||||
import { getEntityFeedLink, getTitleCase } from './EntityUtils';
|
||||
@ -1010,3 +1012,18 @@ export const getTierFromEntityInfo = (entity: FormattedTableData) => {
|
||||
getTierFromSearchTableTags((entity.tags || []).map((tag) => tag.tagFQN))
|
||||
)?.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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user