Revamp: Data quality UI

This commit is contained in:
Shailesh Parmar 2025-06-13 19:57:18 +05:30
parent 9b7b5f2b91
commit b424f0f594
12 changed files with 153 additions and 154 deletions

View File

@ -447,7 +447,11 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
ResourceEntity.TEST_SUITE,
permissions
)}
path={[ROUTES.DATA_QUALITY_WITH_TAB, ROUTES.DATA_QUALITY]}
path={[
ROUTES.DATA_QUALITY_WITH_TAB,
ROUTES.DATA_QUALITY,
ROUTES.DATA_QUALITY_WITH_SUB_TAB,
]}
/>
<AdminProtectedRoute

View File

@ -37,7 +37,7 @@ import {
import QueryString from 'qs';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { useHistory, useParams } from 'react-router-dom';
import { WILD_CARD_CHAR } from '../../../constants/char.constants';
import {
INITIAL_PAGING_VALUE,
@ -61,6 +61,7 @@ import { SearchIndex } from '../../../enums/search.enum';
import { TestCase } from '../../../generated/tests/testCase';
import { usePaging } from '../../../hooks/paging/usePaging';
import useCustomLocation from '../../../hooks/useCustomLocation/useCustomLocation';
import DataQualityClassBase from '../../../pages/DataQuality/DataQualityClassBase';
import { DataQualityPageTabs } from '../../../pages/DataQuality/DataQualityPage.interface';
import { useDataQualityProvider } from '../../../pages/DataQuality/DataQualityProvider';
import { searchQuery } from '../../../rest/searchAPI';
@ -84,15 +85,14 @@ import { SummaryPanel } from '../SummaryPannel/SummaryPanel.component';
export const TestCases = () => {
const [form] = useForm();
const { tab = DataQualityClassBase.getDefaultActiveTab() } =
useParams<{ tab: DataQualityPageTabs }>();
const history = useHistory();
const location = useCustomLocation();
const { t } = useTranslation();
const { permissions } = usePermissionProvider();
const {
isTestCaseSummaryLoading,
testCaseSummary,
activeTab: tab,
} = useDataQualityProvider();
const { isTestCaseSummaryLoading, testCaseSummary } =
useDataQualityProvider();
const { testCase: testCasePermission } = permissions;
const [tableOptions, setTableOptions] = useState<DefaultOptionType[]>([]);
const [isOptionsLoading, setIsOptionsLoading] = useState(false);

View File

@ -10,14 +10,24 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Button, Col, Form, Row, Select, Space, Typography } from 'antd';
import {
Button,
Col,
Form,
Radio,
RadioChangeEvent,
Row,
Select,
Space,
Typography,
} from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { AxiosError } from 'axios';
import { isEmpty } from 'lodash';
import QueryString from 'qs';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useHistory } from 'react-router-dom';
import { Link, useHistory, useParams } from 'react-router-dom';
import { INITIAL_PAGING_VALUE, ROUTES } from '../../../../constants/constants';
import { PROGRESS_BAR_COLOR } from '../../../../constants/TestSuite.constant';
import { usePermissionProvider } from '../../../../context/PermissionProvider/PermissionProvider';
@ -34,7 +44,10 @@ import { EntityReference } from '../../../../generated/entity/type';
import { TestSuite, TestSummary } from '../../../../generated/tests/testCase';
import { usePaging } from '../../../../hooks/paging/usePaging';
import useCustomLocation from '../../../../hooks/useCustomLocation/useCustomLocation';
import { DataQualityPageTabs } from '../../../../pages/DataQuality/DataQualityPage.interface';
import {
DataQualityPageTabs,
DataQualitySubTabs,
} from '../../../../pages/DataQuality/DataQualityPage.interface';
import { useDataQualityProvider } from '../../../../pages/DataQuality/DataQualityProvider';
import {
getListTestSuitesBySearch,
@ -43,6 +56,7 @@ import {
} from '../../../../rest/testAPI';
import { getEntityName } from '../../../../utils/EntityUtils';
import {
getDataQualityPagePath,
getEntityDetailsPath,
getTestSuitePath,
} from '../../../../utils/RouterUtils';
@ -61,13 +75,17 @@ import { SummaryPanel } from '../../SummaryPannel/SummaryPanel.component';
export const TestSuites = () => {
const { t } = useTranslation();
const {
tab = DataQualityPageTabs.TEST_CASES,
subTab = DataQualitySubTabs.TABLE_SUITES,
} = useParams<{
tab?: DataQualityPageTabs;
subTab?: DataQualitySubTabs;
}>();
const history = useHistory();
const location = useCustomLocation();
const {
isTestCaseSummaryLoading,
testCaseSummary,
activeTab: tab,
} = useDataQualityProvider();
const { isTestCaseSummaryLoading, testCaseSummary } =
useDataQualityProvider();
const params = useMemo(() => {
const search = location.search;
@ -181,6 +199,7 @@ export const TestSuites = () => {
return (
<ProfilerProgressWidget
direction="right"
strokeColor={PROGRESS_BAR_COLOR}
value={percent}
/>
@ -205,9 +224,9 @@ export const TestSuites = () => {
q: searchValue ? `*${searchValue}*` : undefined,
owner: ownerFilterValue?.key,
offset: (currentPage - 1) * pageSize,
includeEmptyTestSuites: tab !== DataQualityPageTabs.TABLES,
includeEmptyTestSuites: subTab !== DataQualitySubTabs.TABLE_SUITES,
testSuiteType:
tab === DataQualityPageTabs.TABLES
subTab === DataQualitySubTabs.TABLE_SUITES
? TestSuiteType.basic
: TestSuiteType.logical,
sortField: 'testCaseResultSummary.timestamp',
@ -251,6 +270,12 @@ export const TestSuites = () => {
);
};
const handleSubTabChange = (e: RadioChangeEvent) => {
history.replace(
getDataQualityPagePath(tab, e.target.value as DataQualitySubTabs)
);
};
useEffect(() => {
if (testSuitePermission?.ViewAll || testSuitePermission?.ViewBasic) {
fetchTestSuites(INITIAL_PAGING_VALUE, {
@ -259,7 +284,7 @@ export const TestSuites = () => {
} else {
setIsLoading(false);
}
}, [testSuitePermission, pageSize, searchValue, owner]);
}, [testSuitePermission, pageSize, searchValue, owner, subTab]);
if (!testSuitePermission?.ViewAll && !testSuitePermission?.ViewBasic) {
return (
@ -333,6 +358,27 @@ export const TestSuites = () => {
testSummary={testCaseSummary}
/>
</Col>
<Col span={24}>
<Row gutter={[16, 16]}>
<Col span={16}>
<Radio.Group value={subTab} onChange={handleSubTabChange}>
<Radio.Button value={DataQualitySubTabs.TABLE_SUITES}>
{t('label.table-suite-plural')}
</Radio.Button>
<Radio.Button value={DataQualitySubTabs.BUNDLE_SUITES}>
{t('label.bundle-suite-plural')}
</Radio.Button>
</Radio.Group>
</Col>
<Col span={8}>
<Searchbar
removeMargin
searchValue={searchValue}
onSearch={(value) => handleSearchParam(value, 'searchValue')}
/>
</Col>
</Row>
</Col>
<Col span={24}>
<Table
columns={columns}

View File

@ -26,7 +26,7 @@ const testSuitePermission = {
EditDisplayName: true,
EditCustomFields: true,
};
const mockUseParam = { tab: DataQualityPageTabs.TABLES } as {
const mockUseParam = { tab: DataQualityPageTabs.TEST_CASES } as {
tab?: DataQualityPageTabs;
};
const mockLocation = {
@ -101,7 +101,7 @@ const mockDataQualityContext = {
failed: 0,
skipped: 0,
},
activeTab: DataQualityPageTabs.TABLES,
activeTab: DataQualityPageTabs.TEST_CASES,
};
jest.mock('../../../../pages/DataQuality/DataQualityProvider', () => {
return {

View File

@ -12,23 +12,30 @@
*/
import { Col, Progress, Row } from 'antd';
import classNames from 'classnames';
import React from 'react';
import { ProfilerProgressWidgetProps } from '../TableProfiler.interface';
const ProfilerProgressWidget: React.FC<ProfilerProgressWidgetProps> = ({
value,
strokeColor,
direction = 'left',
}) => {
const modifedValue = Math.round(value * 100);
return (
<Row data-testid="profiler-progress-bar-container">
<Row
className={classNames('flex-row', {
'flex-row-reverse': direction === 'right',
})}
data-testid="profiler-progress-bar-container"
gutter={16}>
<Col span={6}>
<p className="percent-info" data-testid="percent-info">
{`${modifedValue}%`}
</p>
</Col>
<Col span={16}>
<Col span={18}>
<Progress
data-testid="progress-bar"
percent={modifedValue}

View File

@ -73,6 +73,7 @@ export type ModifiedColumn = Column & {
export interface ProfilerProgressWidgetProps {
value: number;
strokeColor?: string;
direction?: 'left' | 'right';
}
export interface ProfilerSettingsModalProps {

View File

@ -250,6 +250,7 @@ export const ROUTES = {
// data quality
DATA_QUALITY: '/data-quality',
DATA_QUALITY_WITH_TAB: `/data-quality/${PLACEHOLDER_ROUTE_TAB}`,
DATA_QUALITY_WITH_SUB_TAB: `/data-quality/${PLACEHOLDER_ROUTE_TAB}/${PLACEHOLDER_ROUTE_SUB_TAB}`,
INCIDENT_MANAGER: '/incident-manager',

View File

@ -163,6 +163,7 @@
"browse-app-plural": "Browse Apps",
"browse-csv-file": "Browse CSV file",
"bulk-edit": "Bulk Edit",
"bundle-suite-plural": "Bundle Suites",
"by-entity": "By {{entity}}",
"by-lowercase": "by",
"ca-certs": "CA Certs",
@ -1373,6 +1374,7 @@
"table-partition-plural": "Table Partitions",
"table-plural": "Tables",
"table-profile": "Table Profile",
"table-suite-plural": "Table Suites",
"table-tests-summary": "Table Tests Summary",
"table-type": "Table Type",
"table-update-plural": "Table Updates",

View File

@ -12,12 +12,10 @@
*/
import { ItemType } from 'antd/lib/menu/hooks/useItems';
import { ReactComponent as TestCaseIcon } from '../../assets/svg/all-activity-v2.svg';
import { ReactComponent as TableIcon } from '../../assets/svg/ic-table.svg';
import { ReactComponent as TestSuiteIcon } from '../../assets/svg/icon-test-suite.svg';
import { TestCases } from '../../components/DataQuality/TestCases/TestCases.component';
import { TestSuites } from '../../components/DataQuality/TestSuite/TestSuiteList/TestSuites.component';
import i18n from '../../utils/i18next/LocalUtil';
import { getDataQualityPagePath } from '../../utils/RouterUtils';
import { DataQualityPageTabs } from './DataQualityPage.interface';
export type DataQualityLeftSideBarType = {
@ -32,20 +30,6 @@ export type DataQualityLeftSideBarType = {
class DataQualityClassBase {
public getLeftSideBar(): DataQualityLeftSideBarType[] {
return [
{
key: DataQualityPageTabs.TABLES,
id: 'by-tables',
label: i18n.t('label.by-entity', {
entity: i18n.t('label.table-plural'),
}),
icon: TableIcon,
description: i18n.t('label.data-health-by-entity', {
entity: i18n.t('label.table-plural'),
}),
iconProps: {
className: 'side-panel-icons',
},
},
{
key: DataQualityPageTabs.TEST_CASES,
label: i18n.t('label.by-entity', {
@ -79,26 +63,21 @@ class DataQualityClassBase {
public getDataQualityTab() {
return [
{
component: TestSuites,
key: DataQualityPageTabs.TABLES,
path: getDataQualityPagePath(DataQualityPageTabs.TABLES),
},
{
key: DataQualityPageTabs.TEST_CASES,
component: TestCases,
path: getDataQualityPagePath(DataQualityPageTabs.TEST_CASES),
label: i18n.t('label.test-case-plural'),
},
{
key: DataQualityPageTabs.TEST_SUITES,
component: TestSuites,
path: getDataQualityPagePath(DataQualityPageTabs.TEST_SUITES),
label: i18n.t('label.test-suite-plural'),
},
];
}
public getDefaultActiveTab(): DataQualityPageTabs {
return DataQualityPageTabs.TABLES;
return DataQualityPageTabs.TEST_CASES;
}
public getManageExtraOptions(_activeTab: DataQualityPageTabs): ItemType[] {

View File

@ -17,11 +17,15 @@ import { TestCaseType } from '../../rest/testAPI';
export enum DataQualityPageTabs {
TEST_SUITES = 'test-suites',
TABLES = 'tables',
TEST_CASES = 'test-cases',
DASHBOARD = 'dashboard',
}
export enum DataQualitySubTabs {
TABLE_SUITES = 'table-suites',
BUNDLE_SUITES = 'bundle-suites',
}
export interface DataQualityContextInterface {
isTestCaseSummaryLoading: boolean;
testCaseSummary: TestSummary;

View File

@ -11,22 +11,13 @@
* limitations under the License.
*/
import { Card, Col, Menu, MenuProps, Row, Typography } from 'antd';
import { Card, Col, Row, Tabs, Typography } from 'antd';
import { isEmpty } from 'lodash';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
Redirect,
Route,
Switch,
useHistory,
useParams,
} from 'react-router-dom';
import { useHistory, useParams } from 'react-router-dom';
import ManageButton from '../../components/common/EntityPageInfos/ManageButton/ManageButton';
import LeftPanelCard from '../../components/common/LeftPanelCard/LeftPanelCard';
import ResizableLeftPanels from '../../components/common/ResizablePanels/ResizableLeftPanels';
import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component';
import { ROUTES } from '../../constants/constants';
import { EntityType } from '../../enums/entity.enum';
import { withPageLayout } from '../../hoc/withPageLayout';
import i18n from '../../utils/i18next/LocalUtil';
@ -37,125 +28,74 @@ import { DataQualityPageTabs } from './DataQualityPage.interface';
import DataQualityProvider from './DataQualityProvider';
const DataQualityPage = () => {
const { tab: activeTab } = useParams<{ tab: DataQualityPageTabs }>();
const { tab: activeTab = DataQualityClassBase.getDefaultActiveTab() } =
useParams<{ tab: DataQualityPageTabs }>();
const history = useHistory();
const { t } = useTranslation();
const menuItems: MenuProps['items'] = useMemo(() => {
const data = DataQualityClassBase.getLeftSideBar();
const menuItems = useMemo(() => {
const data = DataQualityClassBase.getDataQualityTab();
return data.map((value) => {
const SvgIcon = value.icon;
const Component = value.component;
return {
key: value.key,
label: (
<TabsLabel
description={value.description}
id={value.id}
name={value.label}
/>
),
icon: <SvgIcon {...value.iconProps} height={16} width={16} />,
label: <TabsLabel id={value.key} name={value.label} />,
children: <Component />,
};
});
}, []);
const tabDetailsComponent = useMemo(() => {
return DataQualityClassBase.getDataQualityTab();
}, []);
const extraDropdownContent = useMemo(
() => DataQualityClassBase.getManageExtraOptions(activeTab),
[activeTab]
);
const handleTabChange: MenuProps['onClick'] = (event) => {
const activeKey = event.key;
const handleTabChange = (activeKey: string) => {
if (activeKey !== activeTab) {
history.push(getDataQualityPagePath(activeKey as DataQualityPageTabs));
history.replace(getDataQualityPagePath(activeKey as DataQualityPageTabs));
}
};
return (
<div>
<ResizableLeftPanels
className="content-height-with-resizable-panel"
firstPanel={{
className: 'content-resizable-panel-container',
minWidth: 280,
flex: 0.13,
children: (
<LeftPanelCard id="data-quality">
<Menu
className="custom-menu custom-menu-with-description data-quality-page-left-panel-menu"
<Card className="h-full overflow-y-auto">
<DataQualityProvider>
<Row data-testid="data-insight-container" gutter={[0, 16]}>
<Col span={isEmpty(extraDropdownContent) ? 24 : 23}>
<Typography.Title
className="m-b-md"
data-testid="page-title"
level={5}>
{t('label.data-quality')}
</Typography.Title>
<Typography.Paragraph
className="text-grey-muted"
data-testid="page-sub-title">
{t('message.page-sub-header-for-data-quality')}
</Typography.Paragraph>
</Col>
{isEmpty(extraDropdownContent) ? null : (
<Col className="d-flex justify-end" span={1}>
<ManageButton
entityName={EntityType.TEST_CASE}
entityType={EntityType.TEST_CASE}
extraDropdownContent={extraDropdownContent}
/>
</Col>
)}
<Col span={24}>
<Tabs
activeKey={activeTab}
className="tabs-new"
data-testid="tabs"
items={menuItems}
mode="inline"
selectedKeys={[
activeTab ?? DataQualityClassBase.getDefaultActiveTab(),
]}
onClick={handleTabChange}
onChange={handleTabChange}
/>
</LeftPanelCard>
),
}}
pageTitle={t('label.data-quality')}
secondPanel={{
children: (
<Card className="h-full overflow-y-auto">
<DataQualityProvider>
<Row data-testid="data-insight-container" gutter={[0, 16]}>
<Col span={isEmpty(extraDropdownContent) ? 24 : 23}>
<Typography.Title
className="m-b-md"
data-testid="page-title"
level={5}>
{t('label.data-quality')}
</Typography.Title>
<Typography.Paragraph
className="text-grey-muted"
data-testid="page-sub-title">
{t('message.page-sub-header-for-data-quality')}
</Typography.Paragraph>
</Col>
{isEmpty(extraDropdownContent) ? null : (
<Col className="d-flex justify-end" span={1}>
<ManageButton
entityName={EntityType.TEST_CASE}
entityType={EntityType.TEST_CASE}
extraDropdownContent={extraDropdownContent}
/>
</Col>
)}
<Col span={24}>
<Switch>
{tabDetailsComponent.map((tab) => (
<Route
exact
component={tab.component}
key={tab.key}
path={tab.path}
/>
))}
<Route exact path={ROUTES.DATA_QUALITY}>
<Redirect
to={getDataQualityPagePath(
DataQualityClassBase.getDefaultActiveTab()
)}
/>
</Route>
</Switch>
</Col>
</Row>
</DataQualityProvider>
</Card>
),
className: 'content-resizable-panel-container',
minWidth: 800,
flex: 0.87,
}}
/>
</Col>
</Row>
</DataQualityProvider>
</Card>
</div>
);
};

View File

@ -487,13 +487,28 @@ export const getGlossaryTermsVersionsPath = (
return path;
};
export const getDataQualityPagePath = (tab?: DataQualityPageTabs) => {
let path = tab ? ROUTES.DATA_QUALITY_WITH_TAB : ROUTES.DATA_QUALITY;
export const getDataQualityPagePath = (
tab?: DataQualityPageTabs,
subTab?: string
) => {
let path = ROUTES.DATA_QUALITY;
if (tab) {
path = ROUTES.DATA_QUALITY_WITH_TAB;
}
if (subTab) {
path = ROUTES.DATA_QUALITY_WITH_SUB_TAB;
}
if (tab) {
path = path.replace(PLACEHOLDER_ROUTE_TAB, tab);
}
if (subTab) {
path = path.replace(PLACEHOLDER_ROUTE_SUB_TAB, subTab);
}
return path;
};