mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2026-01-08 13:36:32 +00:00
Refactor Data Quality Summary Panel to use SummaryPieChartCard component
- Replaced inline pie chart implementations with a new reusable SummaryPieChartCard component for better code organization and reusability. - Introduced new ChartData and SummaryPieChartCardProps interfaces for type safety. - Updated styles for the new component and adjusted the layout in the DataQuality page. - Added GREY_200 color constant for improved color management in pie charts. - Enhanced percentage calculation utility for better precision and safety in division.
This commit is contained in:
parent
fba8b2c2fc
commit
507df749cb
@ -10,24 +10,19 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
FilterOutlined,
|
||||
SnippetsOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Card, Col, Row, Space, Typography } from 'antd';
|
||||
import { Col, Row } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
import {
|
||||
BLUE_2,
|
||||
GREEN_3,
|
||||
GREY_200,
|
||||
RED_3,
|
||||
YELLOW_2,
|
||||
} from '../../../constants/Color.constants';
|
||||
import { formatNumberWithComma } from '../../../utils/CommonUtils';
|
||||
import './pie-chart-summary-panel.style.less';
|
||||
import { calculatePercentage } from '../../../utils/CommonUtils';
|
||||
import { SummaryPanelProps } from './SummaryPanel.interface';
|
||||
import SummaryPieChartCard from './SummaryPieChartCard/SummaryPieChartCard.component';
|
||||
|
||||
const PieChartSummaryPanel = ({
|
||||
testSummary,
|
||||
@ -45,207 +40,90 @@ const PieChartSummaryPanel = ({
|
||||
totalEntityCount = 0,
|
||||
} = testSummary || {};
|
||||
|
||||
const totalDataAssets = totalDQEntities;
|
||||
const dataAssetsCoverage = totalDQEntities;
|
||||
|
||||
const testData = useMemo(() => {
|
||||
return [
|
||||
const testData = useMemo(
|
||||
() => [
|
||||
{ name: 'Success', value: successTests, color: GREEN_3 },
|
||||
{ name: 'Aborted', value: abortedTests, color: YELLOW_2 },
|
||||
{ name: 'Failed', value: failedTests, color: RED_3 },
|
||||
];
|
||||
}, [successTests, abortedTests, failedTests]);
|
||||
],
|
||||
[successTests, abortedTests, failedTests]
|
||||
);
|
||||
|
||||
const healthyPercentage = useMemo(() => {
|
||||
if (totalDataAssets === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.round((healthyDataAssets / totalDataAssets) * 100);
|
||||
}, [healthyDataAssets, totalDataAssets]);
|
||||
|
||||
const coveragePercentage = useMemo(() => {
|
||||
if (totalEntityCount === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.round((dataAssetsCoverage / totalEntityCount) * 100);
|
||||
}, [dataAssetsCoverage, totalEntityCount]);
|
||||
|
||||
const healthyData = useMemo(() => {
|
||||
const unhealthy = totalDataAssets - healthyDataAssets;
|
||||
|
||||
return [
|
||||
const healthyData = useMemo(
|
||||
() => [
|
||||
{ name: 'Healthy', value: healthyDataAssets, color: GREEN_3 },
|
||||
{ name: 'Unhealthy', value: unhealthy, color: '#E5E7EB' },
|
||||
];
|
||||
}, [healthyDataAssets, totalDataAssets]);
|
||||
{
|
||||
name: 'Unhealthy',
|
||||
value: totalDQEntities - healthyDataAssets,
|
||||
color: GREY_200,
|
||||
},
|
||||
],
|
||||
[healthyDataAssets, totalDQEntities]
|
||||
);
|
||||
|
||||
const coverageData = useMemo(() => {
|
||||
const uncovered = totalEntityCount - dataAssetsCoverage;
|
||||
const coverageData = useMemo(
|
||||
() => [
|
||||
{ name: 'Covered', value: totalDQEntities, color: BLUE_2 },
|
||||
{
|
||||
name: 'Uncovered',
|
||||
value: totalEntityCount - totalDQEntities,
|
||||
color: GREY_200,
|
||||
},
|
||||
],
|
||||
[totalDQEntities, totalEntityCount]
|
||||
);
|
||||
|
||||
return [
|
||||
{ name: 'Covered', value: dataAssetsCoverage, color: BLUE_2 },
|
||||
{ name: 'Uncovered', value: uncovered, color: '#E5E7EB' },
|
||||
];
|
||||
}, [dataAssetsCoverage, totalEntityCount]);
|
||||
const percentages = useMemo(
|
||||
() => ({
|
||||
testSuccess: calculatePercentage(successTests, totalTests),
|
||||
healthy: calculatePercentage(healthyDataAssets, totalDQEntities),
|
||||
coverage: calculatePercentage(totalDQEntities, totalEntityCount),
|
||||
}),
|
||||
[
|
||||
successTests,
|
||||
totalTests,
|
||||
healthyDataAssets,
|
||||
totalDQEntities,
|
||||
totalEntityCount,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<Row className="pie-chart-summary-panel" gutter={[16, 16]}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col md={8} sm={24} xs={24}>
|
||||
<Card className="h-full" loading={isLoading}>
|
||||
<div className="d-flex justify-between items-center">
|
||||
<div className="d-flex items-center gap-4">
|
||||
<FilterOutlined className="summary-icon total-tests-icon" />
|
||||
<div>
|
||||
<Typography.Paragraph className="summary-title">
|
||||
{t('label.total-entity', {
|
||||
entity: t('label.test-plural'),
|
||||
})}
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Typography.Paragraph className="summary-value m-b-0">
|
||||
{formatNumberWithComma(totalTests)}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="chart-container d-flex items-center">
|
||||
<Space className="m-r-md" direction="vertical" size={4}>
|
||||
{testData.map((item) => (
|
||||
<Space key={item.name} size={8}>
|
||||
<div
|
||||
className="legend-dot"
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
<Typography.Text className="legend-text">
|
||||
{item.name} {formatNumberWithComma(item.value)}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
))}
|
||||
</Space>
|
||||
<ResponsiveContainer height={120} width={120}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
data={testData}
|
||||
dataKey="value"
|
||||
innerRadius={35}
|
||||
outerRadius={55}
|
||||
paddingAngle={2}>
|
||||
{testData.map((entry, index) => (
|
||||
<Cell fill={entry.color} key={`cell-${index}`} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
<text
|
||||
className="chart-center-text"
|
||||
dominantBaseline="middle"
|
||||
textAnchor="middle"
|
||||
x="50%"
|
||||
y="50%">
|
||||
100%
|
||||
</text>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<SummaryPieChartCard
|
||||
showLegends
|
||||
chartData={testData}
|
||||
isLoading={isLoading}
|
||||
paddingAngle={2}
|
||||
percentage={percentages.testSuccess}
|
||||
title={t('label.total-entity', {
|
||||
entity: t('label.test-plural'),
|
||||
})}
|
||||
value={totalTests}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
{showAdditionalSummary && (
|
||||
<>
|
||||
<Col md={8} sm={24} xs={24}>
|
||||
<Card className="h-full" loading={isLoading}>
|
||||
<div className="d-flex justify-between items-center">
|
||||
<div className="d-flex items-center gap-4">
|
||||
<CheckCircleOutlined className="summary-icon healthy-icon" />
|
||||
<div>
|
||||
<Typography.Paragraph className="summary-title">
|
||||
{t('label.healthy-data-asset-plural')}
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Typography.Paragraph className="summary-value m-b-0">
|
||||
{formatNumberWithComma(healthyDataAssets)}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
<div className="chart-container">
|
||||
<ResponsiveContainer height={120} width={120}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
data={healthyData}
|
||||
dataKey="value"
|
||||
innerRadius={35}
|
||||
outerRadius={55}
|
||||
paddingAngle={0}>
|
||||
{healthyData.map((entry, index) => (
|
||||
<Cell fill={entry.color} key={`cell-${index}`} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
<text
|
||||
className="chart-center-text"
|
||||
dominantBaseline="middle"
|
||||
textAnchor="middle"
|
||||
x="50%"
|
||||
y="50%">
|
||||
{`${healthyPercentage}%`}
|
||||
</text>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<SummaryPieChartCard
|
||||
chartData={healthyData}
|
||||
isLoading={isLoading}
|
||||
percentage={percentages.healthy}
|
||||
title={t('label.healthy-data-asset-plural')}
|
||||
value={healthyDataAssets}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col md={8} sm={24} xs={24}>
|
||||
<Card className="h-full" loading={isLoading}>
|
||||
<div className="d-flex justify-between items-center">
|
||||
<div className="d-flex items-center gap-4">
|
||||
<SnippetsOutlined className="summary-icon coverage-icon" />
|
||||
<div>
|
||||
<Typography.Paragraph className="summary-title">
|
||||
{t('label.data-asset-plural-coverage')}
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Typography.Paragraph className="summary-value m-b-0">
|
||||
{formatNumberWithComma(dataAssetsCoverage)}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
<div className="chart-container">
|
||||
<ResponsiveContainer height={120} width={120}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
data={coverageData}
|
||||
dataKey="value"
|
||||
innerRadius={35}
|
||||
outerRadius={55}
|
||||
paddingAngle={0}>
|
||||
{coverageData.map((entry, index) => (
|
||||
<Cell fill={entry.color} key={`cell-${index}`} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
<text
|
||||
className="chart-center-text"
|
||||
dominantBaseline="middle"
|
||||
textAnchor="middle"
|
||||
x="50%"
|
||||
y="50%">
|
||||
{`${coveragePercentage}%`}
|
||||
</text>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<SummaryPieChartCard
|
||||
chartData={coverageData}
|
||||
isLoading={isLoading}
|
||||
percentage={percentages.coverage}
|
||||
title={t('label.data-asset-plural-coverage')}
|
||||
value={totalDQEntities}
|
||||
/>
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -17,3 +17,19 @@ export interface SummaryPanelProps {
|
||||
isLoading?: boolean;
|
||||
showAdditionalSummary?: boolean;
|
||||
}
|
||||
|
||||
export interface ChartData {
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface SummaryPieChartCardProps {
|
||||
title: string;
|
||||
value: number;
|
||||
percentage: number;
|
||||
chartData: ChartData[];
|
||||
isLoading?: boolean;
|
||||
showLegends?: boolean;
|
||||
paddingAngle?: number;
|
||||
}
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2024 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 { Card, Space, Typography } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
import { formatNumberWithComma } from '../../../../utils/CommonUtils';
|
||||
import { SummaryPieChartCardProps } from '../SummaryPanel.interface';
|
||||
import './summary-pie-chart-card.style.less';
|
||||
|
||||
const SummaryPieChartCard = ({
|
||||
title,
|
||||
value,
|
||||
percentage,
|
||||
chartData,
|
||||
isLoading = false,
|
||||
showLegends = false,
|
||||
paddingAngle = 0,
|
||||
}: SummaryPieChartCardProps) => {
|
||||
return (
|
||||
<Card className="pie-chart-summary-panel h-full" loading={isLoading}>
|
||||
<div className="d-flex justify-between items-center">
|
||||
<div>
|
||||
<Typography.Paragraph className="summary-title">
|
||||
{title}
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Typography.Paragraph className="summary-value m-b-0">
|
||||
{formatNumberWithComma(value)}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classNames('chart-container', {
|
||||
'd-flex items-center': showLegends,
|
||||
})}>
|
||||
{showLegends && (
|
||||
<Space className="m-r-md" direction="vertical" size={4}>
|
||||
{chartData.map((item) => (
|
||||
<Space key={item.name} size={8}>
|
||||
<div
|
||||
className="legend-dot"
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
<Typography.Paragraph className="text-grey-muted m-b-0">
|
||||
{item.name}{' '}
|
||||
<Typography.Text strong className="text-grey-muted">
|
||||
{formatNumberWithComma(item.value)}
|
||||
</Typography.Text>
|
||||
</Typography.Paragraph>
|
||||
</Space>
|
||||
))}
|
||||
</Space>
|
||||
)}
|
||||
|
||||
<ResponsiveContainer height={120} width={120}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
data={chartData}
|
||||
dataKey="value"
|
||||
innerRadius={45}
|
||||
outerRadius={60}
|
||||
paddingAngle={paddingAngle}>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell fill={entry.color} key={`cell-${index}`} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
<text
|
||||
className="chart-center-text"
|
||||
dominantBaseline="middle"
|
||||
textAnchor="middle"
|
||||
x="50%"
|
||||
y="50%">
|
||||
{`${percentage}%`}
|
||||
</text>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SummaryPieChartCard;
|
||||
@ -11,34 +11,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@import (reference) '../../../styles/variables.less';
|
||||
@import (reference) '../../../../styles/variables.less';
|
||||
|
||||
.pie-chart-summary-panel {
|
||||
.summary-icon {
|
||||
font-size: 28px;
|
||||
|
||||
&.total-tests-icon {
|
||||
color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&.healthy-icon {
|
||||
color: @green-3;
|
||||
background: #f0fdf4;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&.coverage-icon {
|
||||
color: @blue-2;
|
||||
background: #e0f2fe;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-title {
|
||||
font-size: 16px;
|
||||
color: @text-color;
|
||||
@ -54,28 +29,10 @@
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.percentage-change {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&.positive {
|
||||
color: @green-3;
|
||||
}
|
||||
|
||||
&.negative {
|
||||
color: @red-3;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title-value-section {
|
||||
flex-shrink: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.chart-center-text {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
@ -88,9 +45,4 @@
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.legend-text {
|
||||
font-size: 12px;
|
||||
color: @text-grey-muted;
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,6 @@
|
||||
import { RightOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Dropdown,
|
||||
Form,
|
||||
@ -483,204 +482,202 @@ export const TestCases = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Row data-testid="test-case-container" gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Form<TestCaseSearchParams>
|
||||
form={form}
|
||||
layout="horizontal"
|
||||
onValuesChange={handleFilterChange}>
|
||||
<Space wrap align="center" className="w-full" size={16}>
|
||||
<Form.Item className="m-0 w-80">
|
||||
<Searchbar
|
||||
removeMargin
|
||||
placeholder={t('label.search-entity', {
|
||||
entity: t('label.test-case-lowercase'),
|
||||
})}
|
||||
searchValue={searchValue}
|
||||
onSearch={(value) => handleSearchParam('searchValue', value)}
|
||||
<Row data-testid="test-case-container" gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Form<TestCaseSearchParams>
|
||||
form={form}
|
||||
layout="horizontal"
|
||||
onValuesChange={handleFilterChange}>
|
||||
<Space wrap align="center" className="w-full" size={16}>
|
||||
<Form.Item className="m-0 w-80">
|
||||
<Searchbar
|
||||
removeMargin
|
||||
placeholder={t('label.search-entity', {
|
||||
entity: t('label.test-case-lowercase'),
|
||||
})}
|
||||
searchValue={searchValue}
|
||||
onSearch={(value) => handleSearchParam('searchValue', value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle name="selectedFilters">
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: filterMenu,
|
||||
selectedKeys: selectedFilter,
|
||||
onClick: handleMenuClick,
|
||||
}}
|
||||
trigger={['click']}>
|
||||
<Button
|
||||
ghost
|
||||
className="expand-btn"
|
||||
data-testid="advanced-filter"
|
||||
type="primary">
|
||||
{t('label.advanced')}
|
||||
<RightOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Form.Item>
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.table) && (
|
||||
<Form.Item
|
||||
className="m-0 w-80"
|
||||
label={t('label.table')}
|
||||
name="tableFqn">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
data-testid="table-select-filter"
|
||||
loading={isOptionsLoading}
|
||||
options={tableOptions}
|
||||
placeholder={t('label.table')}
|
||||
onSearch={debounceFetchTableData}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle name="selectedFilters">
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: filterMenu,
|
||||
selectedKeys: selectedFilter,
|
||||
onClick: handleMenuClick,
|
||||
}}
|
||||
trigger={['click']}>
|
||||
<Button
|
||||
ghost
|
||||
className="expand-btn"
|
||||
data-testid="advanced-filter"
|
||||
type="primary">
|
||||
{t('label.advanced')}
|
||||
<RightOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.platform) && (
|
||||
<Form.Item
|
||||
className="m-0 w-min-20"
|
||||
label={t('label.platform')}
|
||||
name="testPlatforms">
|
||||
<Select
|
||||
allowClear
|
||||
data-testid="platform-select-filter"
|
||||
mode="multiple"
|
||||
options={TEST_CASE_PLATFORM_OPTION}
|
||||
placeholder={t('label.platform')}
|
||||
/>
|
||||
</Form.Item>
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.table) && (
|
||||
<Form.Item
|
||||
className="m-0 w-80"
|
||||
label={t('label.table')}
|
||||
name="tableFqn">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
data-testid="table-select-filter"
|
||||
loading={isOptionsLoading}
|
||||
options={tableOptions}
|
||||
placeholder={t('label.table')}
|
||||
onSearch={debounceFetchTableData}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.platform) && (
|
||||
<Form.Item
|
||||
className="m-0 w-min-20"
|
||||
label={t('label.platform')}
|
||||
name="testPlatforms">
|
||||
<Select
|
||||
allowClear
|
||||
data-testid="platform-select-filter"
|
||||
mode="multiple"
|
||||
options={TEST_CASE_PLATFORM_OPTION}
|
||||
placeholder={t('label.platform')}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.type) && (
|
||||
<Form.Item
|
||||
className="m-0 w-40"
|
||||
label={t('label.type')}
|
||||
name="testCaseType">
|
||||
<Select
|
||||
allowClear
|
||||
data-testid="test-case-type-select-filter"
|
||||
options={TEST_CASE_TYPE_OPTION}
|
||||
placeholder={t('label.type')}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.status) && (
|
||||
<Form.Item
|
||||
className="m-0 w-40"
|
||||
label={t('label.status')}
|
||||
name="testCaseStatus">
|
||||
<Select
|
||||
allowClear
|
||||
data-testid="status-select-filter"
|
||||
options={TEST_CASE_STATUS_OPTION}
|
||||
placeholder={t('label.status')}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.lastRun) && (
|
||||
<Form.Item
|
||||
className="m-0"
|
||||
label={t('label.last-run')}
|
||||
name="lastRunRange"
|
||||
trigger="handleDateRangeChange"
|
||||
valuePropName="defaultDateRange">
|
||||
<DatePickerMenu showSelectedCustomRange />
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.tags) && (
|
||||
<Form.Item
|
||||
className="m-0 w-80"
|
||||
label={t('label.tag-plural')}
|
||||
name="tags">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
data-testid="tags-select-filter"
|
||||
loading={isOptionsLoading}
|
||||
mode="multiple"
|
||||
options={tagOptions}
|
||||
placeholder={t('label.tag-plural')}
|
||||
onSearch={debounceFetchTagOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.tier) && (
|
||||
<Form.Item
|
||||
className="m-0 w-40"
|
||||
label={t('label.tier')}
|
||||
name="tier">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
data-testid="tier-select-filter"
|
||||
options={tierOptions}
|
||||
placeholder={t('label.tier')}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.service) && (
|
||||
<Form.Item
|
||||
className="m-0 w-80"
|
||||
label={t('label.service')}
|
||||
name="serviceName">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
data-testid="service-select-filter"
|
||||
loading={isOptionsLoading}
|
||||
options={serviceOptions}
|
||||
placeholder={t('label.service')}
|
||||
onSearch={debounceFetchServiceOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.dimension) && (
|
||||
<Form.Item
|
||||
className="m-0 w-80"
|
||||
label={t('label.dimension')}
|
||||
name="dataQualityDimension">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
data-testid="dimension-select-filter"
|
||||
options={TEST_CASE_DIMENSIONS_OPTION}
|
||||
placeholder={t('label.dimension')}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Space>
|
||||
</Form>
|
||||
</Col>
|
||||
{/* <Col span={24}>
|
||||
)}
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.type) && (
|
||||
<Form.Item
|
||||
className="m-0 w-40"
|
||||
label={t('label.type')}
|
||||
name="testCaseType">
|
||||
<Select
|
||||
allowClear
|
||||
data-testid="test-case-type-select-filter"
|
||||
options={TEST_CASE_TYPE_OPTION}
|
||||
placeholder={t('label.type')}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.status) && (
|
||||
<Form.Item
|
||||
className="m-0 w-40"
|
||||
label={t('label.status')}
|
||||
name="testCaseStatus">
|
||||
<Select
|
||||
allowClear
|
||||
data-testid="status-select-filter"
|
||||
options={TEST_CASE_STATUS_OPTION}
|
||||
placeholder={t('label.status')}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.lastRun) && (
|
||||
<Form.Item
|
||||
className="m-0"
|
||||
label={t('label.last-run')}
|
||||
name="lastRunRange"
|
||||
trigger="handleDateRangeChange"
|
||||
valuePropName="defaultDateRange">
|
||||
<DatePickerMenu showSelectedCustomRange />
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.tags) && (
|
||||
<Form.Item
|
||||
className="m-0 w-80"
|
||||
label={t('label.tag-plural')}
|
||||
name="tags">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
data-testid="tags-select-filter"
|
||||
loading={isOptionsLoading}
|
||||
mode="multiple"
|
||||
options={tagOptions}
|
||||
placeholder={t('label.tag-plural')}
|
||||
onSearch={debounceFetchTagOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.tier) && (
|
||||
<Form.Item
|
||||
className="m-0 w-40"
|
||||
label={t('label.tier')}
|
||||
name="tier">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
data-testid="tier-select-filter"
|
||||
options={tierOptions}
|
||||
placeholder={t('label.tier')}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.service) && (
|
||||
<Form.Item
|
||||
className="m-0 w-80"
|
||||
label={t('label.service')}
|
||||
name="serviceName">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
data-testid="service-select-filter"
|
||||
loading={isOptionsLoading}
|
||||
options={serviceOptions}
|
||||
placeholder={t('label.service')}
|
||||
onSearch={debounceFetchServiceOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{selectedFilter.includes(TEST_CASE_FILTERS.dimension) && (
|
||||
<Form.Item
|
||||
className="m-0 w-80"
|
||||
label={t('label.dimension')}
|
||||
name="dataQualityDimension">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
data-testid="dimension-select-filter"
|
||||
options={TEST_CASE_DIMENSIONS_OPTION}
|
||||
placeholder={t('label.dimension')}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Space>
|
||||
</Form>
|
||||
</Col>
|
||||
{/* <Col span={24}>
|
||||
<SummaryPanel
|
||||
showAdditionalSummary
|
||||
isLoading={isTestCaseSummaryLoading}
|
||||
testSummary={testCaseSummary}
|
||||
/>
|
||||
</Col> */}
|
||||
<Col span={24}>
|
||||
<PieChartSummaryPanel
|
||||
isLoading={isTestCaseSummaryLoading}
|
||||
testSummary={testCaseSummary}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<DataQualityTab
|
||||
afterDeleteAction={fetchTestCases}
|
||||
breadcrumbData={[
|
||||
{
|
||||
name: t('label.data-quality'),
|
||||
url: getDataQualityPagePath(DataQualityPageTabs.TEST_CASES),
|
||||
},
|
||||
]}
|
||||
fetchTestCases={sortTestCase}
|
||||
isLoading={isLoading}
|
||||
pagingData={pagingData}
|
||||
showPagination={showPagination}
|
||||
testCases={testCase}
|
||||
onTestCaseResultUpdate={handleStatusSubmit}
|
||||
onTestUpdate={handleTestCaseUpdate}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
<Col span={24}>
|
||||
<PieChartSummaryPanel
|
||||
isLoading={isTestCaseSummaryLoading}
|
||||
testSummary={testCaseSummary}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<DataQualityTab
|
||||
afterDeleteAction={fetchTestCases}
|
||||
breadcrumbData={[
|
||||
{
|
||||
name: t('label.data-quality'),
|
||||
url: getDataQualityPagePath(DataQualityPageTabs.TEST_CASES),
|
||||
},
|
||||
]}
|
||||
fetchTestCases={sortTestCase}
|
||||
isLoading={isLoading}
|
||||
pagingData={pagingData}
|
||||
showPagination={showPagination}
|
||||
testCases={testCase}
|
||||
onTestCaseResultUpdate={handleStatusSubmit}
|
||||
onTestUpdate={handleTestCaseUpdate}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
@ -71,7 +71,7 @@ import { UserTeamSelectableList } from '../../../common/UserTeamSelectableList/U
|
||||
import { TableProfilerTab } from '../../../Database/Profiler/ProfilerDashboard/profilerDashboard.interface';
|
||||
import ProfilerProgressWidget from '../../../Database/Profiler/TableProfiler/ProfilerProgressWidget/ProfilerProgressWidget';
|
||||
import { TestSuiteSearchParams } from '../../DataQuality.interface';
|
||||
import PieChartSummaryPanel from '../../SummaryPannel/PieChartSummaryPanel.component';
|
||||
import { SummaryPanel } from '../../SummaryPannel/SummaryPanel.component';
|
||||
|
||||
export const TestSuites = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -352,15 +352,15 @@ export const TestSuites = () => {
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
{/* <SummaryPanel
|
||||
<SummaryPanel
|
||||
showAdditionalSummary
|
||||
isLoading={isTestCaseSummaryLoading}
|
||||
testSummary={testCaseSummary}
|
||||
/> */}
|
||||
<PieChartSummaryPanel
|
||||
/>
|
||||
{/* <PieChartSummaryPanel
|
||||
isLoading={isTestCaseSummaryLoading}
|
||||
testSummary={testCaseSummary}
|
||||
/>
|
||||
/> */}
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Row gutter={[16, 16]}>
|
||||
|
||||
@ -37,3 +37,4 @@ export const DESERT = '#B56727';
|
||||
export const PINK_SALMON = '#FF92AE';
|
||||
export const ELECTRIC_VIOLET = '#9747FF';
|
||||
export const LEMON_ZEST = '#FFD700';
|
||||
export const GREY_200 = '#E9EAEB';
|
||||
|
||||
@ -112,7 +112,7 @@ const DataQualityPage = () => {
|
||||
<Col span={24}>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
className="tabs-new data-quality-page-tabs"
|
||||
className="tabs-new data-quality-page-tabs tab-bg-blank"
|
||||
data-testid="tabs"
|
||||
items={menuItems}
|
||||
onChange={handleTabChange}
|
||||
|
||||
@ -13,8 +13,9 @@
|
||||
|
||||
@import (reference) '../../styles/variables.less';
|
||||
|
||||
.data-quality-page-tabs {
|
||||
.data-quality-page-tabs.ant-tabs.tabs-new {
|
||||
.ant-tabs-tabpane {
|
||||
margin-top: @size-md;
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ import {
|
||||
isNull,
|
||||
isString,
|
||||
isUndefined,
|
||||
round,
|
||||
toLower,
|
||||
toNumber,
|
||||
} from 'lodash';
|
||||
@ -833,3 +834,26 @@ export const calculatePercentageFromValue = (
|
||||
) => {
|
||||
return (value * percentageValue) / 100;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates percentage from numerator and denominator with safe division
|
||||
* @param numerator - The numerator value
|
||||
* @param denominator - The denominator value
|
||||
* @param precision - Number of decimal places to round to (default: 1)
|
||||
* @returns Calculated percentage rounded to specified precision, or 0 if denominator is 0
|
||||
* @example
|
||||
* calculatePercentage(25, 100) // returns 25.0
|
||||
* calculatePercentage(1, 3, 2) // returns 33.33
|
||||
* calculatePercentage(5, 0) // returns 0 (safe division)
|
||||
*/
|
||||
export const calculatePercentage = (
|
||||
numerator: number,
|
||||
denominator: number,
|
||||
precision = 1
|
||||
): number => {
|
||||
if (denominator === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return round((numerator / denominator) * 100, precision);
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user