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:
Shailesh Parmar 2025-06-25 18:52:58 +05:30
parent fba8b2c2fc
commit 507df749cb
10 changed files with 405 additions and 439 deletions

View File

@ -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>
</>
)}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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>
);
};

View File

@ -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]}>

View File

@ -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';

View File

@ -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}

View File

@ -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;
}
}

View File

@ -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);
};