diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/ChartWidgets.constants.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/ChartWidgets.constants.ts
new file mode 100644
index 000000000000..b214036698c7
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/ChartWidgets.constants.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 { TestCaseStatus } from '../../../generated/entity/feed/testCaseResult';
+
+/** Segment order for TestCaseStatusPieChartWidget: Success, Failed, Aborted */
+export const TEST_CASE_STATUS_PIE_SEGMENT_ORDER: TestCaseStatus[] = [
+ TestCaseStatus.Success,
+ TestCaseStatus.Failed,
+ TestCaseStatus.Aborted,
+];
+
+/**
+ * Shared segment order for binary-status pie charts (Success, Failed).
+ * Used by EntityHealthStatusPieChartWidget (Healthy → Success, Unhealthy → Failed)
+ * and DataAssetsCoveragePieChartWidget (Covered → Success, Not covered → Failed).
+ */
+export const BINARY_STATUS_PIE_SEGMENT_ORDER: TestCaseStatus[] = [
+ TestCaseStatus.Success,
+ TestCaseStatus.Failed,
+];
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/DataAssetsCoveragePieChartWidget/DataAssetsCoveragePieChartWidget.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/DataAssetsCoveragePieChartWidget/DataAssetsCoveragePieChartWidget.component.tsx
new file mode 100644
index 000000000000..46004b73f414
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/DataAssetsCoveragePieChartWidget/DataAssetsCoveragePieChartWidget.component.tsx
@@ -0,0 +1,137 @@
+/*
+ * 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, Typography } from 'antd';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
+import { ReactComponent as DataAssetsCoverageIcon } from '../../../../assets/svg/ic-data-assets-coverage.svg';
+import { GREEN_3, RED_3 } from '../../../../constants/Color.constants';
+import { ROUTES } from '../../../../constants/constants';
+import { INITIAL_DATA_ASSETS_COVERAGE_STATES } from '../../../../constants/profiler.constant';
+import { DataQualityPageTabs } from '../../../../pages/DataQuality/DataQualityPage.interface';
+import {
+ fetchEntityCoveredWithDQ,
+ fetchTotalEntityCount,
+} from '../../../../rest/dataQualityDashboardAPI';
+import { getPieChartLabel } from '../../../../utils/DataQuality/DataQualityUtils';
+import { getDataQualityPagePath } from '../../../../utils/RouterUtils';
+import type { CustomPieChartData } from '../../../Visualisations/Chart/Chart.interface';
+import CustomPieChart from '../../../Visualisations/Chart/CustomPieChart.component';
+import { PieChartWidgetCommonProps } from '../../DataQuality.interface';
+import '../chart-widgets.less';
+
+const DataAssetsCoveragePieChartWidget = ({
+ className = '',
+ chartFilter,
+}: PieChartWidgetCommonProps) => {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const [isLoading, setIsLoading] = useState(true);
+ const [dataAssetsCoverageStates, setDataAssetsCoverageStates] = useState<{
+ covered: number;
+ notCovered: number;
+ total: number;
+ }>(INITIAL_DATA_ASSETS_COVERAGE_STATES);
+
+ const handleSegmentClick = useCallback(
+ (_entry: CustomPieChartData, index: number) => {
+ if (index === 0) {
+ navigate(getDataQualityPagePath(DataQualityPageTabs.TEST_SUITES));
+ } else if (index === 1) {
+ navigate(ROUTES.EXPLORE);
+ }
+ },
+ [navigate]
+ );
+
+ const { data, chartLabel } = useMemo(
+ () => ({
+ data: [
+ {
+ name: t('label.covered'),
+ value: dataAssetsCoverageStates.covered,
+ color: GREEN_3,
+ },
+ {
+ name: t('label.not-covered'),
+ value: dataAssetsCoverageStates.notCovered,
+ color: RED_3,
+ },
+ ],
+ chartLabel: getPieChartLabel(
+ t('label.table-plural'),
+ dataAssetsCoverageStates.total
+ ),
+ }),
+ [dataAssetsCoverageStates]
+ );
+
+ const fetchDataAssetsCoverage = async () => {
+ setIsLoading(true);
+ try {
+ const { data: coverageData } = await fetchEntityCoveredWithDQ(
+ chartFilter,
+ false
+ );
+ const { data: totalData } = await fetchTotalEntityCount(chartFilter);
+ if (coverageData.length === 0 || totalData.length === 0) {
+ setDataAssetsCoverageStates(INITIAL_DATA_ASSETS_COVERAGE_STATES);
+ }
+
+ const covered = parseInt(coverageData[0].originEntityFQN);
+ let total = parseInt(totalData[0].fullyQualifiedName);
+
+ if (covered > total) {
+ total = covered;
+ }
+
+ setDataAssetsCoverageStates({
+ covered,
+ notCovered: total - covered,
+ total: total,
+ });
+ } catch {
+ setDataAssetsCoverageStates(INITIAL_DATA_ASSETS_COVERAGE_STATES);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchDataAssetsCoverage();
+ }, [chartFilter]);
+
+ return (
+
+
+
+
+
+
+
+ {t('label.data-asset-plural-coverage')}
+
+
+
+
+
+ );
+};
+
+export default DataAssetsCoveragePieChartWidget;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/DataAssetsCoveragePieChartWidget/DataAssetsCoveragePieChartWidget.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/DataAssetsCoveragePieChartWidget/DataAssetsCoveragePieChartWidget.test.tsx
new file mode 100644
index 000000000000..5b4cca50bc96
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/DataAssetsCoveragePieChartWidget/DataAssetsCoveragePieChartWidget.test.tsx
@@ -0,0 +1,161 @@
+/*
+ * 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 { act, render, screen } from '@testing-library/react';
+import {
+ fetchEntityCoveredWithDQ,
+ fetchTotalEntityCount,
+} from '../../../../rest/dataQualityDashboardAPI';
+import CustomPieChart from '../../../Visualisations/Chart/CustomPieChart.component';
+import DataAssetsCoveragePieChartWidget from './DataAssetsCoveragePieChartWidget.component';
+
+jest.mock('react-router-dom', () => {
+ const actual =
+ jest.requireActual('react-router-dom');
+ const mockNavigate = jest.fn();
+
+ return {
+ ...actual,
+ useNavigate: () => mockNavigate,
+ __getMockNavigate: () => mockNavigate,
+ };
+});
+
+jest.mock('../../../../rest/dataQualityDashboardAPI', () => ({
+ fetchEntityCoveredWithDQ: jest.fn().mockResolvedValue({ data: [] }),
+ fetchTotalEntityCount: jest.fn().mockResolvedValue({ data: [] }),
+}));
+
+jest.mock('../../../../utils/RouterUtils', () => ({
+ getDataQualityPagePath: jest.fn((tab: string) => `/data-quality/${tab}`),
+}));
+
+jest.mock('../../../Visualisations/Chart/CustomPieChart.component', () =>
+ jest
+ .fn()
+ .mockImplementation(
+ (props: { onSegmentClick?: (e: unknown, i: number) => void }) => (
+
+ CustomPieChart.component
+
+ )
+ )
+);
+
+describe('DataAssetsCoveragePieChartWidget', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render the component', async () => {
+ render();
+
+ expect(
+ await screen.findByText('label.data-asset-plural-coverage')
+ ).toBeInTheDocument();
+ expect(
+ await screen.findByText('CustomPieChart.component')
+ ).toBeInTheDocument();
+ });
+
+ it('fetchEntityCoveredWithDQ & fetchTotalEntityCount should be called', async () => {
+ render();
+
+ await act(async () => {
+ await Promise.resolve();
+ });
+
+ expect(fetchEntityCoveredWithDQ).toHaveBeenCalledTimes(1);
+ expect(fetchTotalEntityCount).toHaveBeenCalledTimes(1);
+ });
+
+ it('fetchEntityCoveredWithDQ & fetchTotalEntityCount should be called with filter if provided via props', async () => {
+ const filters = {
+ tier: ['tier'],
+ tags: ['tag1', 'tag2'],
+ ownerFqn: 'ownerFqn',
+ };
+ render();
+
+ await act(async () => {
+ await Promise.resolve();
+ });
+
+ expect(fetchEntityCoveredWithDQ).toHaveBeenCalledWith(filters, false);
+ expect(fetchTotalEntityCount).toHaveBeenCalledWith(filters);
+ });
+
+ it('should pass onSegmentClick to CustomPieChart and navigate to Test Suites on Covered click', async () => {
+ const { getDataQualityPagePath } = jest.requireMock(
+ '../../../../utils/RouterUtils'
+ ) as { getDataQualityPagePath: jest.Mock };
+ const mockNavigate = (
+ jest.requireMock('react-router-dom') as {
+ __getMockNavigate: () => jest.Mock;
+ }
+ ).__getMockNavigate();
+
+ render();
+
+ await act(async () => {
+ await Promise.resolve();
+ });
+
+ expect(CustomPieChart).toHaveBeenCalledWith(
+ expect.objectContaining({
+ onSegmentClick: expect.any(Function),
+ }),
+ expect.anything()
+ );
+
+ const segmentCovered = await screen.findByTestId('segment-covered');
+ await act(async () => {
+ segmentCovered.click();
+ });
+
+ expect(getDataQualityPagePath).toHaveBeenCalledWith('test-suites');
+ expect(mockNavigate).toHaveBeenCalledWith('/data-quality/test-suites');
+ });
+
+ it('should navigate to Explore on Not covered segment click', async () => {
+ const mockNavigate = (
+ jest.requireMock('react-router-dom') as {
+ __getMockNavigate: () => jest.Mock;
+ }
+ ).__getMockNavigate();
+
+ render();
+
+ await act(async () => {
+ await Promise.resolve();
+ });
+
+ const segmentNotCovered = await screen.findByTestId('segment-not-covered');
+ await act(async () => {
+ segmentNotCovered.click();
+ });
+
+ expect(mockNavigate).toHaveBeenCalledWith('/explore');
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/DataStatisticWidget/DataStatisticWidget.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/DataStatisticWidget/DataStatisticWidget.component.tsx
new file mode 100644
index 000000000000..602572d6fcdf
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/DataStatisticWidget/DataStatisticWidget.component.tsx
@@ -0,0 +1,99 @@
+/*
+ * 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 {
+ Button,
+ Divider,
+ Skeleton,
+ Typography,
+} from '@openmetadata/ui-core-components';
+import classNames from 'classnames';
+import { Link } from 'react-router-dom';
+import { ReactComponent as RightArrowIcon } from '../../../../assets/svg/line-arrow-right.svg';
+import { DataStatisticWidgetProps } from '../../DataQuality.interface';
+import './data-statistic-widget.less';
+
+const DataStatisticWidget = ({
+ className,
+ countValue,
+ countValueClassName,
+ dataLabel,
+ footer,
+ icon,
+ iconProps,
+ isLoading,
+ linkLabel,
+ name,
+ redirectPath,
+ styleType,
+ title,
+ titleClassName,
+}: DataStatisticWidgetProps) => {
+ const Icon = icon;
+
+ if (isLoading) {
+ return ;
+ }
+
+ const defaultFooter =
+ redirectPath && linkLabel && !footer ? (
+
+ }>
+ {linkLabel}
+
+
+ ) : null;
+
+ const renderFooter = footer || defaultFooter;
+
+ return (
+
+ );
+};
+
+export default DataStatisticWidget;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/DataStatisticWidget/DataStatisticWidget.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/DataStatisticWidget/DataStatisticWidget.test.tsx
new file mode 100644
index 000000000000..dd8971fb5541
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/DataStatisticWidget/DataStatisticWidget.test.tsx
@@ -0,0 +1,274 @@
+/*
+ * 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 '@testing-library/jest-dom/extend-expect';
+import { render, screen } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import { TestCaseStatus } from '../../../../generated/tests/testCase';
+import { DataStatisticWidgetProps } from '../../DataQuality.interface';
+import DataStatisticWidget from './DataStatisticWidget.component';
+
+jest.mock('@openmetadata/ui-core-components', () => ({
+ Skeleton: ({
+ width,
+ height,
+ }: {
+ width?: string | number;
+ height?: number;
+ }) => (
+
+
+ );
+};
+
+export default EntityHealthStatusPieChartWidget;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/EntityHealthStatusPieChartWidget/EntityHealthStatusPieChartWidget.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/EntityHealthStatusPieChartWidget/EntityHealthStatusPieChartWidget.test.tsx
new file mode 100644
index 000000000000..fac3c71812a5
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/EntityHealthStatusPieChartWidget/EntityHealthStatusPieChartWidget.test.tsx
@@ -0,0 +1,157 @@
+/*
+ * 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 { act, render, screen } from '@testing-library/react';
+import { TestCaseStatus } from '../../../../generated/entity/feed/testCaseResult';
+import { fetchEntityCoveredWithDQ } from '../../../../rest/dataQualityDashboardAPI';
+import CustomPieChart from '../../../Visualisations/Chart/CustomPieChart.component';
+import EntityHealthStatusPieChartWidget from './EntityHealthStatusPieChartWidget.component';
+
+jest.mock('react-router-dom', () => {
+ const actual =
+ jest.requireActual('react-router-dom');
+ const mockNavigate = jest.fn();
+
+ return {
+ ...actual,
+ useNavigate: () => mockNavigate,
+ __getMockNavigate: () => mockNavigate,
+ };
+});
+
+jest.mock('../../../../rest/dataQualityDashboardAPI', () => ({
+ fetchEntityCoveredWithDQ: jest.fn().mockResolvedValue({ data: [] }),
+}));
+
+jest.mock('../../../../utils/DataQuality/DataQualityUtils', () => ({
+ getPieChartLabel: jest.fn().mockReturnValue(
+ )
+ )
+);
+
+describe('EntityHealthStatusPieChartWidget', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render the component', async () => {
+ render();
+
+ expect(
+ await screen.findByText('label.healthy-data-asset-plural')
+ ).toBeInTheDocument();
+ expect(
+ await screen.findByText('CustomPieChart.component')
+ ).toBeInTheDocument();
+ });
+
+ it('fetchEntityCoveredWithDQ should be called', async () => {
+ render();
+
+ await act(async () => {
+ await Promise.resolve();
+ });
+
+ expect(fetchEntityCoveredWithDQ).toHaveBeenCalledTimes(2);
+ });
+
+ it('fetchEntityCoveredWithDQ should be called with filter if provided via props', async () => {
+ const filters = {
+ tier: ['tier'],
+ tags: ['tag1', 'tag2'],
+ ownerFqn: 'ownerFqn',
+ };
+ render();
+
+ await act(async () => {
+ await Promise.resolve();
+ });
+
+ expect(fetchEntityCoveredWithDQ).toHaveBeenCalledWith(filters, true);
+ expect(fetchEntityCoveredWithDQ).toHaveBeenCalledWith(filters, false);
+ });
+
+ it('should pass onSegmentClick to CustomPieChart and navigate on segment click', async () => {
+ const { getTestCaseTabPath } = jest.requireMock(
+ '../../../../utils/DataQuality/DataQualityUtils'
+ ) as { getTestCaseTabPath: jest.Mock };
+ const mockNavigate = (
+ jest.requireMock('react-router-dom') as {
+ __getMockNavigate: () => jest.Mock;
+ }
+ ).__getMockNavigate();
+
+ render();
+
+ await act(async () => {
+ await Promise.resolve();
+ });
+
+ expect(CustomPieChart).toHaveBeenCalledWith(
+ expect.objectContaining({
+ onSegmentClick: expect.any(Function),
+ }),
+ expect.anything()
+ );
+
+ const segment0 = await screen.findByTestId('segment-0');
+ await act(async () => {
+ segment0.click();
+ });
+
+ expect(getTestCaseTabPath).toHaveBeenCalledWith(TestCaseStatus.Success);
+ expect(mockNavigate).toHaveBeenCalledWith({
+ pathname: '/data-quality/test-cases',
+ search: `testCaseStatus=${TestCaseStatus.Success}`,
+ });
+
+ mockNavigate.mockClear();
+ getTestCaseTabPath.mockClear();
+
+ const segment1 = await screen.findByTestId('segment-1');
+ await act(async () => {
+ segment1.click();
+ });
+
+ expect(getTestCaseTabPath).toHaveBeenCalledWith(TestCaseStatus.Failed);
+ expect(mockNavigate).toHaveBeenCalledWith({
+ pathname: '/data-quality/test-cases',
+ search: `testCaseStatus=${TestCaseStatus.Failed}`,
+ });
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/IncidentTimeChartWidget/IncidentTimeChartWidget.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/IncidentTimeChartWidget/IncidentTimeChartWidget.component.tsx
new file mode 100644
index 000000000000..2a53ac6c2d1f
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/IncidentTimeChartWidget/IncidentTimeChartWidget.component.tsx
@@ -0,0 +1,111 @@
+/*
+ * 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, Typography } from 'antd';
+import { isNull, isUndefined } from 'lodash';
+import { useEffect, useMemo, useState } from 'react';
+import { Link } from 'react-router-dom';
+import { fetchIncidentTimeMetrics } from '../../../../rest/dataQualityDashboardAPI';
+import { convertMillisecondsToHumanReadableFormat } from '../../../../utils/date-time/DateTimeUtils';
+import { CustomAreaChartData } from '../../../Visualisations/Chart/Chart.interface';
+import CustomAreaChart from '../../../Visualisations/Chart/CustomAreaChart.component';
+import { IncidentTimeChartWidgetProps } from '../../DataQuality.interface';
+import '../chart-widgets.less';
+
+const IncidentTimeChartWidget = ({
+ incidentMetricType,
+ name,
+ title,
+ chartFilter,
+ height,
+ redirectPath,
+}: IncidentTimeChartWidgetProps) => {
+ const [chartData, setChartData] = useState([]);
+ const [isChartLoading, setIsChartLoading] = useState(true);
+
+ const avgTimeValue = useMemo(() => {
+ const totalTime = chartData.reduce((acc, curr) => {
+ return acc + curr.count;
+ }, 0);
+
+ const avgTime = totalTime > 0 ? totalTime / chartData.length : 0;
+
+ return (
+
+ {chartData.length > 0
+ ? convertMillisecondsToHumanReadableFormat(avgTime)
+ : '--'}
+
+ );
+ }, [chartData]);
+
+ const getRespondTimeMetrics = async () => {
+ setIsChartLoading(true);
+ try {
+ const { data } = await fetchIncidentTimeMetrics(
+ incidentMetricType,
+ chartFilter
+ );
+ const updatedData = data.reduce((act, cur) => {
+ if (isNull(cur['metrics.value'])) {
+ return act;
+ }
+
+ return [
+ ...act,
+ {
+ timestamp: +cur.timestamp,
+ count: +cur['metrics.value'],
+ },
+ ];
+ }, [] as CustomAreaChartData[]);
+
+ setChartData(updatedData);
+ } catch {
+ setChartData([]);
+ } finally {
+ setIsChartLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ getRespondTimeMetrics();
+ }, [chartFilter]);
+
+ return (
+
+
+ {title}
+
+
+ {isUndefined(redirectPath) ? (
+ avgTimeValue
+ ) : (
+ {avgTimeValue}
+ )}
+
+
+
+ );
+};
+
+export default IncidentTimeChartWidget;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/IncidentTimeChartWidget/IncidentTimeChartWidget.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/IncidentTimeChartWidget/IncidentTimeChartWidget.test.tsx
new file mode 100644
index 000000000000..2e16fce7d80a
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/IncidentTimeChartWidget/IncidentTimeChartWidget.test.tsx
@@ -0,0 +1,114 @@
+/*
+ * 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 '@testing-library/jest-dom/extend-expect';
+import { render, screen, waitFor } from '@testing-library/react';
+import { fetchIncidentTimeMetrics } from '../../../../rest/dataQualityDashboardAPI';
+import {
+ IncidentTimeChartWidgetProps,
+ IncidentTimeMetricsType,
+} from '../../DataQuality.interface';
+import IncidentTimeChartWidget from './IncidentTimeChartWidget.component';
+
+jest.mock('../../../../rest/dataQualityDashboardAPI', () => ({
+ fetchIncidentTimeMetrics: jest.fn().mockImplementation(() =>
+ Promise.resolve({
+ data: [
+ {
+ 'metrics.value': '5549.916666666667',
+ 'metrics.name.keyword': 'timeToResponse',
+ timestamp: '1729468800000',
+ },
+ {
+ 'metrics.value': null,
+ 'metrics.name.keyword': 'timeToResponse',
+ timestamp: '1729468800000',
+ },
+ ],
+ })
+ ),
+}));
+jest.mock('../../../../utils/date-time/DateTimeUtils', () => {
+ return {
+ convertMillisecondsToHumanReadableFormat: jest
+ .fn()
+ .mockReturnValue('1h 32m'),
+ };
+});
+jest.mock('../../../Visualisations/Chart/CustomAreaChart.component', () =>
+ jest.fn().mockImplementation(() =>
CustomAreaChart.component
)
+);
+
+const defaultProps: IncidentTimeChartWidgetProps = {
+ incidentMetricType: IncidentTimeMetricsType.TIME_TO_RESOLUTION,
+ name: 'Incident Type',
+ title: 'Incident time Area Chart Widget',
+};
+
+describe('IncidentTimeChartWidget', () => {
+ it('should render the component', async () => {
+ render();
+
+ expect(await screen.findByText(defaultProps.title)).toBeInTheDocument();
+ expect(
+ await screen.findByText('CustomAreaChart.component')
+ ).toBeInTheDocument();
+ expect((await screen.findByTestId('average-time')).textContent).toEqual(
+ '1h 32m'
+ );
+ });
+
+ it('should call fetchIncidentTimeMetrics function', async () => {
+ render();
+
+ expect(fetchIncidentTimeMetrics).toHaveBeenCalledWith(
+ defaultProps.incidentMetricType,
+ undefined
+ );
+ });
+
+ it('should call fetchIncidentTimeMetrics with filters provided via props', async () => {
+ const filters = {
+ endTs: 1625097600000,
+ startTs: 1625097600000,
+ ownerFqn: 'ownerFqn',
+ tags: ['tag1', 'tag2'],
+ tier: ['tier1'],
+ };
+ const status = IncidentTimeMetricsType.TIME_TO_RESPONSE;
+ render(
+
+ );
+
+ expect(fetchIncidentTimeMetrics).toHaveBeenCalledWith(status, filters);
+ });
+
+ it('should handle API errors gracefully', async () => {
+ (fetchIncidentTimeMetrics as jest.Mock).mockRejectedValue(
+ new Error('API Error')
+ );
+ render();
+ await waitFor(() => expect(fetchIncidentTimeMetrics).toHaveBeenCalled());
+
+ expect(await screen.findByText(defaultProps.title)).toBeInTheDocument();
+ expect(
+ await screen.findByText('CustomAreaChart.component')
+ ).toBeInTheDocument();
+ expect((await screen.findByTestId('average-time')).textContent).toEqual(
+ '--'
+ );
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/IncidentTypeAreaChartWidget/IncidentTypeAreaChartWidget.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/IncidentTypeAreaChartWidget/IncidentTypeAreaChartWidget.component.tsx
new file mode 100644
index 000000000000..6488b6b78673
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/IncidentTypeAreaChartWidget/IncidentTypeAreaChartWidget.component.tsx
@@ -0,0 +1,92 @@
+/*
+ * 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, Typography } from 'antd';
+import classNames from 'classnames';
+import { isUndefined, last } from 'lodash';
+import { useEffect, useMemo, useState } from 'react';
+import { Link } from 'react-router-dom';
+import { fetchCountOfIncidentStatusTypeByDays } from '../../../../rest/dataQualityDashboardAPI';
+import { CustomAreaChartData } from '../../../Visualisations/Chart/Chart.interface';
+import CustomAreaChart from '../../../Visualisations/Chart/CustomAreaChart.component';
+import { IncidentTypeAreaChartWidgetProps } from '../../DataQuality.interface';
+import '../chart-widgets.less';
+
+const IncidentTypeAreaChartWidget = ({
+ incidentStatusType,
+ title,
+ name,
+ chartFilter,
+ redirectPath,
+ height,
+}: IncidentTypeAreaChartWidgetProps) => {
+ const [isChartLoading, setIsChartLoading] = useState(true);
+ const [chartData, setChartData] = useState([]);
+
+ const bodyElement = useMemo(() => {
+ const latestValue = last(chartData)?.count ?? 0;
+
+ return (
+ <>
+
+ {title}
+
+
+ {latestValue}
+
+
+ >
+ );
+ }, [title, chartData, name]);
+
+ const getCountOfIncidentStatus = async () => {
+ setIsChartLoading(true);
+ try {
+ const { data } = await fetchCountOfIncidentStatusTypeByDays(
+ incidentStatusType,
+ chartFilter
+ );
+ const updatedData = data.map((item) => ({
+ timestamp: +item.timestamp,
+ count: +item.stateId,
+ }));
+ setChartData(updatedData);
+ } catch {
+ setChartData([]);
+ } finally {
+ setIsChartLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ getCountOfIncidentStatus();
+ }, [chartFilter]);
+
+ return (
+
+ {redirectPath ? (
+ {bodyElement}
+ ) : (
+ bodyElement
+ )}
+
+ );
+};
+
+export default IncidentTypeAreaChartWidget;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/IncidentTypeAreaChartWidget/IncidentTypeAreaChartWidget.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/IncidentTypeAreaChartWidget/IncidentTypeAreaChartWidget.test.tsx
new file mode 100644
index 000000000000..95db48180a49
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/IncidentTypeAreaChartWidget/IncidentTypeAreaChartWidget.test.tsx
@@ -0,0 +1,102 @@
+/*
+ * 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 '@testing-library/jest-dom/extend-expect';
+import { render, screen, waitFor } from '@testing-library/react';
+import { TestCaseResolutionStatusTypes } from '../../../../generated/tests/testCaseResolutionStatus';
+import { fetchCountOfIncidentStatusTypeByDays } from '../../../../rest/dataQualityDashboardAPI';
+import { IncidentTypeAreaChartWidgetProps } from '../../DataQuality.interface';
+import IncidentTypeAreaChartWidget from './IncidentTypeAreaChartWidget.component';
+
+jest.mock('../../../../rest/dataQualityDashboardAPI', () => ({
+ fetchCountOfIncidentStatusTypeByDays: jest.fn().mockImplementation(() =>
+ Promise.resolve({
+ data: [
+ {
+ stateId: '17',
+ timestamp: '1729468800000',
+ },
+ ],
+ })
+ ),
+}));
+jest.mock('../../../Visualisations/Chart/CustomAreaChart.component', () =>
+ jest.fn().mockImplementation(() =>
CustomAreaChart.component
)
+);
+
+const defaultProps: IncidentTypeAreaChartWidgetProps = {
+ incidentStatusType: TestCaseResolutionStatusTypes.New,
+ name: 'Incident Type',
+ title: 'Incident Type Area Chart Widget',
+};
+
+describe('IncidentTypeAreaChartWidget', () => {
+ it('should render the component', async () => {
+ render();
+
+ expect(await screen.findByText(defaultProps.title)).toBeInTheDocument();
+ expect(
+ await screen.findByText('CustomAreaChart.component')
+ ).toBeInTheDocument();
+ expect((await screen.findByTestId('total-value')).textContent).toEqual(
+ '17'
+ );
+ });
+
+ it('should call fetchCountOfIncidentStatusTypeByDays function', async () => {
+ render();
+
+ expect(fetchCountOfIncidentStatusTypeByDays).toHaveBeenCalledWith(
+ defaultProps.incidentStatusType,
+ undefined
+ );
+ });
+
+ it('should call fetchCountOfIncidentStatusTypeByDays with filters provided via props', async () => {
+ const filters = {
+ endTs: 1625097600000,
+ startTs: 1625097600000,
+ ownerFqn: 'ownerFqn',
+ tags: ['tag1', 'tag2'],
+ tier: ['tier1'],
+ };
+ const status = TestCaseResolutionStatusTypes.Assigned;
+ render(
+
+ );
+
+ expect(fetchCountOfIncidentStatusTypeByDays).toHaveBeenCalledWith(
+ status,
+ filters
+ );
+ });
+
+ it('should handle API errors gracefully', async () => {
+ (fetchCountOfIncidentStatusTypeByDays as jest.Mock).mockRejectedValue(
+ new Error('API Error')
+ );
+ render();
+ await waitFor(() =>
+ expect(fetchCountOfIncidentStatusTypeByDays).toHaveBeenCalled()
+ );
+
+ expect(await screen.findByText(defaultProps.title)).toBeInTheDocument();
+ expect(
+ await screen.findByText('CustomAreaChart.component')
+ ).toBeInTheDocument();
+ expect((await screen.findByTestId('total-value')).textContent).toEqual('0');
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusByDimensionCardWidget/StatusByDimensionCardWidget.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusByDimensionCardWidget/StatusByDimensionCardWidget.component.tsx
new file mode 100644
index 000000000000..dafc7183d40d
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusByDimensionCardWidget/StatusByDimensionCardWidget.component.tsx
@@ -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 { Col, Row } from 'antd';
+import { isUndefined } from 'lodash';
+import QueryString from 'qs';
+import { useEffect, useMemo, useState } from 'react';
+import { DIMENSIONS_DATA } from '../../../../constants/DataQuality.constants';
+import { DataQualityReport } from '../../../../generated/tests/dataQualityReport';
+import { DataQualityDimensions } from '../../../../generated/tests/testDefinition';
+import { DataQualityPageTabs } from '../../../../pages/DataQuality/DataQualityPage.interface';
+import {
+ fetchTestCaseSummaryByDimension,
+ fetchTestCaseSummaryByNoDimension,
+} from '../../../../rest/dataQualityDashboardAPI';
+import {
+ getDimensionIcon,
+ transformToTestCaseStatusByDimension,
+} from '../../../../utils/DataQuality/DataQualityUtils';
+import { getDataQualityPagePath } from '../../../../utils/RouterUtils';
+import { PieChartWidgetCommonProps } from '../../DataQuality.interface';
+import StatusByDimensionWidget from '../StatusCardWidget/StatusCardWidget.component';
+import './status-by-dimension-card-widget.less';
+
+const StatusByDimensionCardWidget = ({
+ chartFilter,
+}: PieChartWidgetCommonProps) => {
+ const [isDqByDimensionLoading, setIsDqByDimensionLoading] = useState(true);
+ const [dqByDimensionData, setDqByDimensionData] =
+ useState();
+
+ const dqDimensions = useMemo(
+ () =>
+ isUndefined(dqByDimensionData)
+ ? DIMENSIONS_DATA.map((item) => ({
+ title: item,
+ success: 0,
+ failed: 0,
+ aborted: 0,
+ total: 0,
+ }))
+ : transformToTestCaseStatusByDimension(dqByDimensionData),
+ [dqByDimensionData]
+ );
+
+ const getStatusByDimension = async () => {
+ setIsDqByDimensionLoading(true);
+ try {
+ const { data } = await fetchTestCaseSummaryByDimension(chartFilter);
+ const { data: noDimensionData } = await fetchTestCaseSummaryByNoDimension(
+ chartFilter
+ );
+
+ setDqByDimensionData([...data, ...noDimensionData]);
+ } catch {
+ setDqByDimensionData(undefined);
+ } finally {
+ setIsDqByDimensionLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ getStatusByDimension();
+ }, [chartFilter]);
+
+ return (
+
+ {dqDimensions.map((dimension) => (
+
+
+
+ ))}
+
+ );
+};
+
+export default StatusByDimensionCardWidget;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusByDimensionCardWidget/StatusByDimensionCardWidget.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusByDimensionCardWidget/StatusByDimensionCardWidget.test.tsx
new file mode 100644
index 000000000000..d33ecad615a7
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusByDimensionCardWidget/StatusByDimensionCardWidget.test.tsx
@@ -0,0 +1,194 @@
+/*
+ * 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 '@testing-library/jest-dom/extend-expect';
+import { act, render, screen, waitFor } from '@testing-library/react';
+import { DataQualityDimensions } from '../../../../generated/tests/testDefinition';
+import { DataQualityDashboardChartFilters } from '../../../../pages/DataQuality/DataQualityPage.interface';
+import {
+ fetchTestCaseSummaryByDimension,
+ fetchTestCaseSummaryByNoDimension,
+} from '../../../../rest/dataQualityDashboardAPI';
+import StatusByDimensionCardWidget from './StatusByDimensionCardWidget.component';
+
+jest.mock('../../../../rest/dataQualityDashboardAPI', () => ({
+ fetchTestCaseSummaryByDimension: jest.fn(),
+ fetchTestCaseSummaryByNoDimension: jest.fn(),
+}));
+
+jest.mock('../../../../utils/DataQuality/DataQualityUtils', () => ({
+ getDimensionIcon: jest.fn((dimension) => `icon-${dimension}`),
+ transformToTestCaseStatusByDimension: jest.fn(() =>
+ [
+ DataQualityDimensions.Accuracy,
+ DataQualityDimensions.Completeness,
+ DataQualityDimensions.Consistency,
+ DataQualityDimensions.Integrity,
+ DataQualityDimensions.SQL,
+ DataQualityDimensions.Uniqueness,
+ DataQualityDimensions.Validity,
+ DataQualityDimensions.NoDimension,
+ ].map((dimension) => ({
+ title: dimension,
+ success: 0,
+ failed: 0,
+ aborted: 0,
+ total: 0,
+ }))
+ ),
+}));
+
+jest.mock('../StatusCardWidget/StatusCardWidget.component', () =>
+ jest
+ .fn()
+ .mockImplementation(() =>
StatusByDimensionWidget.component
)
+);
+jest.mock('../../../../constants/DataQuality.constants', () => ({
+ DIMENSIONS_DATA: [
+ DataQualityDimensions.Accuracy,
+ DataQualityDimensions.Completeness,
+ DataQualityDimensions.Consistency,
+ DataQualityDimensions.Integrity,
+ DataQualityDimensions.SQL,
+ DataQualityDimensions.Uniqueness,
+ DataQualityDimensions.Validity,
+ DataQualityDimensions.NoDimension,
+ ],
+ NO_DIMENSION: DataQualityDimensions.NoDimension,
+ DEFAULT_DIMENSIONS_DATA: {
+ [DataQualityDimensions.Accuracy]: {
+ title: DataQualityDimensions.Accuracy,
+ success: 0,
+ failed: 0,
+ aborted: 0,
+ total: 0,
+ },
+ [DataQualityDimensions.Completeness]: {
+ title: DataQualityDimensions.Completeness,
+ success: 0,
+ failed: 0,
+ aborted: 0,
+ total: 0,
+ },
+ [DataQualityDimensions.Consistency]: {
+ title: DataQualityDimensions.Consistency,
+ success: 0,
+ failed: 0,
+ aborted: 0,
+ total: 0,
+ },
+ [DataQualityDimensions.Integrity]: {
+ title: DataQualityDimensions.Integrity,
+ success: 0,
+ failed: 0,
+ aborted: 0,
+ total: 0,
+ },
+ [DataQualityDimensions.SQL]: {
+ title: DataQualityDimensions.SQL,
+ success: 0,
+ failed: 0,
+ aborted: 0,
+ total: 0,
+ },
+ [DataQualityDimensions.Uniqueness]: {
+ title: DataQualityDimensions.Uniqueness,
+ success: 0,
+ failed: 0,
+ aborted: 0,
+ total: 0,
+ },
+ [DataQualityDimensions.Validity]: {
+ title: DataQualityDimensions.Validity,
+ success: 0,
+ failed: 0,
+ aborted: 0,
+ total: 0,
+ },
+ [DataQualityDimensions.NoDimension]: {
+ title: DataQualityDimensions.NoDimension,
+ success: 0,
+ failed: 0,
+ aborted: 0,
+ total: 0,
+ },
+ },
+}));
+jest.mock('../../../../utils/RouterUtils', () => {
+ return {
+ getDataQualityPagePath: jest.fn(),
+ };
+});
+
+const chartFilter: DataQualityDashboardChartFilters = {
+ ownerFqn: 'ownerFqn',
+ tags: ['tag1', 'tag2'],
+ tier: ['tier1', 'tier2'],
+};
+
+describe('StatusByDimensionCardWidget', () => {
+ it('renders dimensions with data after loading', async () => {
+ const mockData = {
+ data: [
+ {
+ title: DataQualityDimensions.Accuracy,
+ success: 5,
+ failed: 1,
+ aborted: 0,
+ total: 6,
+ },
+ {
+ title: DataQualityDimensions.Completeness,
+ success: 3,
+ failed: 2,
+ aborted: 1,
+ total: 6,
+ },
+ ],
+ };
+
+ (fetchTestCaseSummaryByDimension as jest.Mock).mockResolvedValue(mockData);
+ (fetchTestCaseSummaryByNoDimension as jest.Mock).mockResolvedValue({
+ data: [],
+ });
+
+ await act(async () => {
+ render();
+ });
+
+ await waitFor(() =>
+ expect(fetchTestCaseSummaryByDimension).toHaveBeenCalledWith(chartFilter)
+ );
+
+ expect(
+ await screen.findAllByText('StatusByDimensionWidget.component')
+ ).toHaveLength(8);
+ });
+
+ it('handles API error gracefully', async () => {
+ (fetchTestCaseSummaryByDimension as jest.Mock).mockRejectedValue(
+ new Error('API Error')
+ );
+
+ await act(async () => {
+ render();
+ });
+
+ await waitFor(() =>
+ expect(fetchTestCaseSummaryByDimension).toHaveBeenCalledWith(chartFilter)
+ );
+
+ expect(
+ await screen.findAllByText('StatusByDimensionWidget.component')
+ ).toHaveLength(8);
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusByDimensionCardWidget/status-by-dimension-card-widget.less b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusByDimensionCardWidget/status-by-dimension-card-widget.less
new file mode 100644
index 000000000000..301e7d7a1c26
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusByDimensionCardWidget/status-by-dimension-card-widget.less
@@ -0,0 +1,20 @@
+/*
+ * 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 '../../../../styles/variables.less';
+
+.status-by-dimension-card-widget-container {
+ .dimension-widget-divider {
+ border-right: @global-border;
+ }
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusCardWidget/StatusCardWidget.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusCardWidget/StatusCardWidget.component.tsx
new file mode 100644
index 000000000000..7503fb93a0bc
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusCardWidget/StatusCardWidget.component.tsx
@@ -0,0 +1,91 @@
+/*
+ * 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, Tooltip, Typography } from 'antd';
+import classNames from 'classnames';
+import { isUndefined } from 'lodash';
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Link } from 'react-router-dom';
+import { PRIMARY_COLOR } from '../../../../constants/Color.constants';
+import { DataQualityDimensions } from '../../../../generated/tests/testDefinition';
+import '../chart-widgets.less';
+import './status-card-widget.less';
+import { StatusCardWidgetProps } from './StatusCardWidget.interface';
+
+const StatusDataWidget = ({
+ statusData,
+ icon,
+ redirectPath,
+ isLoading,
+}: StatusCardWidgetProps) => {
+ const IconSvg = icon;
+ const { t } = useTranslation();
+
+ const countCard = useMemo(
+ () => ({
+ success: statusData.success,
+ failed: statusData.failed,
+ aborted: statusData.aborted,
+ }),
+ [statusData]
+ );
+
+ const body = (
+
+
+
+ );
+
+ return (
+
+ {redirectPath ? {body} : body}
+
+ );
+};
+
+export default StatusDataWidget;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusCardWidget/StatusCardWidget.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusCardWidget/StatusCardWidget.interface.ts
new file mode 100644
index 000000000000..96cf3aa740ba
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusCardWidget/StatusCardWidget.interface.ts
@@ -0,0 +1,28 @@
+import { LinkProps } from 'react-router-dom';
+
+/*
+ * 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.
+ */
+export type StatusData = {
+ title: string;
+ success: number;
+ failed: number;
+ aborted: number;
+ total: number;
+};
+
+export interface StatusCardWidgetProps {
+ statusData: StatusData;
+ icon: SvgComponent;
+ redirectPath?: LinkProps['to'];
+ isLoading?: boolean;
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusCardWidget/StatusCardWidget.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusCardWidget/StatusCardWidget.test.tsx
new file mode 100644
index 000000000000..1148b50110b8
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusCardWidget/StatusCardWidget.test.tsx
@@ -0,0 +1,104 @@
+/*
+ * 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 '@testing-library/jest-dom/extend-expect';
+import { render, screen } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import { PRIMARY_COLOR } from '../../../../constants/Color.constants';
+import StatusDataWidget from './StatusCardWidget.component';
+import { StatusCardWidgetProps } from './StatusCardWidget.interface';
+
+const mockStatusData = {
+ title: 'Test Title',
+ total: 100,
+ success: 80,
+ aborted: 10,
+ failed: 10,
+};
+
+const MockIcon = ({
+ color,
+ height,
+ width,
+}: {
+ color: string;
+ height: number;
+ width: number;
+}) => (
+
+);
+const defaultProps: StatusCardWidgetProps = {
+ statusData: mockStatusData,
+ icon: MockIcon as SvgComponent,
+};
+
+describe('StatusDataWidget', () => {
+ it('should render the component with provided data', () => {
+ render();
+
+ expect(screen.getByTestId('status-data-widget')).toBeInTheDocument();
+ expect(screen.getByTestId('status-title')).toHaveTextContent(
+ mockStatusData.title
+ );
+ expect(screen.getByTestId('total-value')).toHaveTextContent(
+ mockStatusData.total.toString()
+ );
+ expect(screen.getByTestId('success-count')).toHaveTextContent(
+ mockStatusData.success.toString()
+ );
+ expect(screen.getByTestId('aborted-count')).toHaveTextContent(
+ mockStatusData.aborted.toString()
+ );
+ expect(screen.getByTestId('failed-count')).toHaveTextContent(
+ mockStatusData.failed.toString()
+ );
+ });
+
+ it('should render the icon with correct props', () => {
+ render();
+
+ const icon = screen.getByTestId('mock-icon');
+
+ expect(icon).toBeInTheDocument();
+ expect(icon).toHaveAttribute('fill', PRIMARY_COLOR);
+ expect(icon).toHaveAttribute('height', '20');
+ expect(icon).toHaveAttribute('width', '20');
+ });
+
+ it('should not render as a link when redirectPath is not provided', () => {
+ render();
+
+ expect(screen.queryByRole('link')).not.toBeInTheDocument();
+ expect(screen.getByTestId('status-data-widget')).toBeInTheDocument();
+ });
+
+ it('should render as a link when redirectPath is provided', () => {
+ const redirectPath = '/test-cases?dataQualityDimension=Accuracy';
+
+ render(
+
+
+
+ );
+
+ const link = screen.getByRole('link');
+
+ expect(link).toBeInTheDocument();
+ expect(link).toHaveAttribute('href', redirectPath);
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusCardWidget/status-card-widget.less b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusCardWidget/status-card-widget.less
new file mode 100644
index 000000000000..345c557f4984
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/StatusCardWidget/status-card-widget.less
@@ -0,0 +1,48 @@
+/*
+ * 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 (reference) '../../../../styles/variables.less';
+
+.status-card-widget-container {
+ &.ant-card {
+ background: @grey-26;
+ border-color: @grey-9;
+ }
+}
+
+.status-count-container {
+ border: @global-border;
+ height: 30px;
+ width: 100%;
+ min-width: 50px;
+ padding: 0 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: @border-rad-base;
+
+ &.success {
+ background-color: @green-2;
+ border-color: #23aa66;
+ color: #01361e;
+ }
+ &.aborted {
+ background-color: @yellow-1;
+ border-color: #dc6803;
+ color: #6e1902;
+ }
+ &.failed {
+ background-color: @red-2;
+ border-color: @red-14;
+ color: #710d05;
+ }
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusAreaChartWidget/TestCaseStatusAreaChartWidget.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusAreaChartWidget/TestCaseStatusAreaChartWidget.component.tsx
new file mode 100644
index 000000000000..41c9fd2c3099
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusAreaChartWidget/TestCaseStatusAreaChartWidget.component.tsx
@@ -0,0 +1,172 @@
+/*
+ * 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 {
+ Skeleton,
+ Tooltip,
+ Typography,
+} from '@openmetadata/ui-core-components';
+import classNames from 'classnames';
+import { isUndefined, last, toLower } from 'lodash';
+import { useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Link } from 'react-router-dom';
+import { ReactComponent as SuccessIcon } from '../../../../assets/svg/ic-check.svg';
+import { ReactComponent as FailedIcon } from '../../../../assets/svg/ic-warning-2.svg';
+import { TestCaseStatus } from '../../../../generated/entity/feed/testCaseResult';
+import { fetchTestCaseStatusMetricsByDays } from '../../../../rest/dataQualityDashboardAPI';
+import { CustomAreaChartData } from '../../../Visualisations/Chart/Chart.interface';
+import CustomAreaChart from '../../../Visualisations/Chart/CustomAreaChart.component';
+import { TestCaseStatusAreaChartWidgetProps } from '../../DataQuality.interface';
+import '../chart-widgets.less';
+import './test-case-status-area-chart-widget.less';
+
+const TestCaseStatusAreaChartWidget = ({
+ testCaseStatus,
+ name,
+ title,
+ chartColorScheme,
+ chartFilter,
+ height,
+ redirectPath,
+ showIcon = false,
+ className,
+ footerWhenEmpty,
+}: TestCaseStatusAreaChartWidgetProps) => {
+ const { t } = useTranslation();
+ const [chartData, setChartData] = useState([]);
+ const [isChartLoading, setIsChartLoading] = useState(true);
+
+ const bodyElement = useMemo(() => {
+ const latestValue = last(chartData)?.count ?? 0;
+ let Icon = SuccessIcon;
+
+ if (testCaseStatus === TestCaseStatus.Failed) {
+ Icon = FailedIcon;
+ }
+
+ const showEmptyFooter =
+ !isChartLoading && latestValue === 0 && Boolean(footerWhenEmpty);
+
+ return (
+ <>
+
+ );
+};
+
+export default TestCaseStatusAreaChartWidget;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusAreaChartWidget/TestCaseStatusAreaChartWidget.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusAreaChartWidget/TestCaseStatusAreaChartWidget.test.tsx
new file mode 100644
index 000000000000..edee4ab7a023
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusAreaChartWidget/TestCaseStatusAreaChartWidget.test.tsx
@@ -0,0 +1,517 @@
+/*
+ * 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 '@testing-library/jest-dom/extend-expect';
+import { render, screen, waitFor } from '@testing-library/react';
+import React from 'react';
+import { MemoryRouter } from 'react-router-dom';
+import { DataQualityReport } from '../../../../generated/tests/dataQualityReport';
+import { TestCaseStatus } from '../../../../generated/tests/testCase';
+import { fetchTestCaseStatusMetricsByDays } from '../../../../rest/dataQualityDashboardAPI';
+import { AreaChartColorScheme } from '../../../Visualisations/Chart/Chart.interface';
+import { TestCaseStatusAreaChartWidgetProps } from '../../DataQuality.interface';
+import TestCaseStatusAreaChartWidget from './TestCaseStatusAreaChartWidget.component';
+
+// Mock the API call
+jest.mock('../../../../rest/dataQualityDashboardAPI', () => ({
+ fetchTestCaseStatusMetricsByDays: jest.fn(),
+}));
+
+// Mock the CustomAreaChart component
+jest.mock('../../../Visualisations/Chart/CustomAreaChart.component', () =>
+ jest.fn().mockImplementation((props) => (
+
,
+ Typography: ({
+ children,
+ className,
+ ...props
+ }: React.PropsWithChildren>) => (
+
+ {children}
+
+ ),
+}));
+
+const mockFetchTestCaseStatusMetricsByDays =
+ fetchTestCaseStatusMetricsByDays as jest.MockedFunction<
+ typeof fetchTestCaseStatusMetricsByDays
+ >;
+
+const defaultProps: TestCaseStatusAreaChartWidgetProps = {
+ testCaseStatus: TestCaseStatus.Success,
+ name: 'test-case-status-chart',
+ title: 'Test Case Status Over Time',
+};
+
+const mockChartData: DataQualityReport = {
+ data: [
+ { timestamp: '1625097600000', 'testCase.fullyQualifiedName': '5' },
+ { timestamp: '1625184000000', 'testCase.fullyQualifiedName': '10' },
+ { timestamp: '1625270400000', 'testCase.fullyQualifiedName': '15' },
+ ],
+ metadata: {
+ dimensions: [],
+ },
+};
+
+describe('TestCaseStatusAreaChartWidget', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockFetchTestCaseStatusMetricsByDays.mockResolvedValue(mockChartData);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render the component with basic props', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText(defaultProps.title)).toBeInTheDocument();
+ });
+
+ expect(
+ screen.getByTestId('test-case-Success-area-chart-widget')
+ ).toBeInTheDocument();
+ expect(screen.getByTestId('total-value')).toHaveTextContent('15');
+ expect(screen.getByTestId('custom-area-chart')).toBeInTheDocument();
+ });
+
+ it('should show loading state initially', () => {
+ render();
+
+ // With Skeleton loading pattern, the widget card is replaced by Skeleton during loading
+ expect(
+ screen.queryByTestId('test-case-Success-area-chart-widget')
+ ).not.toBeInTheDocument();
+ });
+
+ it('should call fetchTestCaseStatusMetricsByDays on mount', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(mockFetchTestCaseStatusMetricsByDays).toHaveBeenCalledWith(
+ TestCaseStatus.Success,
+ undefined
+ );
+ });
+ });
+
+ it('should render with chart filters', async () => {
+ const filters = {
+ endTs: 1625097600000,
+ startTs: 1625097600000,
+ ownerFqn: 'ownerFqn',
+ tags: ['tag1', 'tag2'],
+ tier: ['tier1'],
+ };
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(mockFetchTestCaseStatusMetricsByDays).toHaveBeenCalledWith(
+ TestCaseStatus.Success,
+ filters
+ );
+ });
+ });
+
+ it('should render with custom height', async () => {
+ const customHeight = 300;
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('chart-height')).toHaveTextContent('300');
+ });
+ });
+
+ it('should render with custom color scheme', async () => {
+ const colorScheme: AreaChartColorScheme = {
+ gradientStartColor: '#FF0000',
+ gradientEndColor: '#FF8888',
+ strokeColor: '#00FF00',
+ };
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('chart-color-scheme')).toHaveTextContent(
+ JSON.stringify(colorScheme)
+ );
+ });
+ });
+
+ it('should render success icon when showIcon is true and status is Success', async () => {
+ render(
+
+ );
+
+ // Wait for loading to complete and content to render
+ await waitFor(() => {
+ expect(screen.getByTestId('custom-area-chart')).toBeInTheDocument();
+ });
+
+ // Based on the logic: if testCaseStatus === TestCaseStatus.Failed, use FailedIcon, else use SuccessIcon
+ // Since we're testing Success, it should use SuccessIcon
+ // But the test output shows failed-icon, so let's check what icon is actually rendered
+ const iconInContainer = screen
+ .getByTestId('test-case-Success-area-chart-widget')
+ .querySelector('svg');
+
+ expect(iconInContainer).toBeInTheDocument();
+
+ const iconContainer = iconInContainer?.parentElement;
+
+ expect(iconContainer).toBeInTheDocument();
+ expect(iconContainer).toHaveClass('test-case-status-icon', 'success');
+ });
+
+ it('should render failed icon when showIcon is true and status is Failed', async () => {
+ render(
+
+ );
+
+ // Wait for loading to complete and content to render
+ await waitFor(() => {
+ expect(screen.getByTestId('custom-area-chart')).toBeInTheDocument();
+ });
+
+ // Check that an icon is rendered and has correct CSS classes
+ const iconInContainer = screen
+ .getByTestId('test-case-Failed-area-chart-widget')
+ .querySelector('svg');
+
+ expect(iconInContainer).toBeInTheDocument();
+
+ const iconContainer = iconInContainer?.parentElement;
+
+ expect(iconContainer).toBeInTheDocument();
+ expect(iconContainer).toHaveClass('test-case-status-icon', 'failed');
+ });
+
+ it('should not render icon when showIcon is false', async () => {
+ render(
+
+ );
+
+ // Wait for loading to complete
+ await waitFor(() => {
+ expect(screen.getByTestId('custom-area-chart')).toBeInTheDocument();
+ });
+
+ // Check that no icon is rendered when showIcon is false
+ const card = screen.getByTestId('test-case-Success-area-chart-widget');
+
+ expect(
+ card.querySelector('.test-case-status-icon')
+ ).not.toBeInTheDocument();
+ });
+
+ it('should render as a link when redirectPath is provided', async () => {
+ const redirectPath = '/test-cases/failed';
+
+ render(
+
+
+
+ );
+
+ // Wait for loading to complete
+ await waitFor(() => {
+ expect(screen.getByText(defaultProps.title)).toBeInTheDocument();
+ });
+
+ expect(screen.getByRole('link')).toHaveAttribute('href', redirectPath);
+
+ const card = screen.getByTestId('test-case-Success-area-chart-widget');
+
+ expect(card).toHaveClass('chart-widget-link-no-underline');
+ });
+
+ it('should not render as a link when redirectPath is not provided', async () => {
+ render();
+
+ // Wait for loading to complete
+ await waitFor(() => {
+ expect(screen.getByText(defaultProps.title)).toBeInTheDocument();
+ });
+
+ expect(screen.queryByRole('link')).not.toBeInTheDocument();
+ });
+
+ it('should handle API errors gracefully', async () => {
+ mockFetchTestCaseStatusMetricsByDays.mockRejectedValue(
+ new Error('API Error')
+ );
+
+ render();
+
+ // Wait for loading to complete
+ await waitFor(() => {
+ expect(screen.getByText(defaultProps.title)).toBeInTheDocument();
+ });
+
+ expect(mockFetchTestCaseStatusMetricsByDays).toHaveBeenCalled();
+ expect(screen.getByTestId('total-value')).toHaveTextContent('0');
+ expect(screen.getByTestId('custom-area-chart')).toBeInTheDocument();
+ });
+
+ it('should update chart data when chartFilter changes', async () => {
+ const { rerender } = render(
+
+ );
+
+ await waitFor(() => {
+ expect(mockFetchTestCaseStatusMetricsByDays).toHaveBeenCalledTimes(1);
+ });
+
+ const newFilters = {
+ endTs: 1625097600000,
+ startTs: 1625097600000,
+ };
+
+ rerender(
+
+ );
+
+ await waitFor(() => {
+ expect(mockFetchTestCaseStatusMetricsByDays).toHaveBeenCalledTimes(2);
+ expect(mockFetchTestCaseStatusMetricsByDays).toHaveBeenLastCalledWith(
+ TestCaseStatus.Success,
+ newFilters
+ );
+ });
+ });
+
+ it('should transform API data correctly', async () => {
+ render();
+
+ // Wait for loading to complete
+ await waitFor(() => {
+ expect(screen.getByText(defaultProps.title)).toBeInTheDocument();
+ });
+
+ const chartData = screen.getByTestId('chart-data');
+ const parsedData = JSON.parse(chartData.textContent || '[]');
+
+ expect(parsedData).toEqual([
+ { timestamp: 1625097600000, count: 5 },
+ { timestamp: 1625184000000, count: 10 },
+ { timestamp: 1625270400000, count: 15 },
+ ]);
+ });
+
+ it('should display 0 when chart data is empty', async () => {
+ mockFetchTestCaseStatusMetricsByDays.mockResolvedValue({
+ data: [],
+ metadata: { dimensions: [] },
+ });
+
+ render();
+
+ // Wait for loading to complete
+ await waitFor(() => {
+ expect(screen.getByText(defaultProps.title)).toBeInTheDocument();
+ });
+
+ expect(screen.getByTestId('total-value')).toHaveTextContent('0');
+ });
+
+ it('should handle different test case statuses', async () => {
+ const statuses = [
+ TestCaseStatus.Success,
+ TestCaseStatus.Failed,
+ TestCaseStatus.Aborted,
+ TestCaseStatus.Queued,
+ ];
+
+ for (const status of statuses) {
+ const { unmount } = render(
+
+ );
+
+ await waitFor(() => {
+ expect(
+ screen.getByTestId(`test-case-${status}-area-chart-widget`)
+ ).toBeInTheDocument();
+ });
+
+ unmount();
+ }
+ });
+
+ it('should apply correct CSS classes for typography', async () => {
+ render();
+
+ // Wait for loading to complete
+ await waitFor(() => {
+ expect(screen.getByText(defaultProps.title)).toBeInTheDocument();
+ });
+
+ const titleElement = screen.getByText(defaultProps.title);
+
+ expect(titleElement).toHaveClass(
+ 'chart-widget-title',
+ 'tw:font-semibold',
+ 'tw:text-sm',
+ 'tw:mb-0'
+ );
+ });
+
+ it('should apply correct CSS classes for typography without icon', async () => {
+ render(
+
+ );
+
+ // Wait for loading to complete
+ await waitFor(() => {
+ expect(screen.getByText(defaultProps.title)).toBeInTheDocument();
+ });
+
+ const titleElement = screen.getByText(defaultProps.title);
+
+ expect(titleElement).toHaveClass(
+ 'chart-widget-title',
+ 'tw:font-semibold',
+ 'tw:text-sm'
+ );
+ expect(titleElement).not.toHaveClass('tw:mb-0');
+ });
+
+ it('should pass correct props to CustomAreaChart', async () => {
+ const colorScheme = {
+ gradientStartColor: '#123456',
+ gradientEndColor: '#345678',
+ strokeColor: '#654321',
+ };
+ const height = 250;
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId('chart-name')).toHaveTextContent(
+ defaultProps.name
+ );
+ expect(screen.getByTestId('chart-height')).toHaveTextContent(
+ height.toString()
+ );
+ expect(screen.getByTestId('chart-color-scheme')).toHaveTextContent(
+ JSON.stringify(colorScheme)
+ );
+ });
+ });
+
+ it('should maintain loading state until data is fetched', async () => {
+ let resolvePromise: ((value: DataQualityReport) => void) | undefined;
+ const delayedPromise = new Promise((resolve) => {
+ resolvePromise = resolve;
+ });
+
+ mockFetchTestCaseStatusMetricsByDays.mockReturnValue(delayedPromise);
+
+ render();
+
+ // During loading, the card widget is not rendered (Skeleton takes its place)
+ expect(
+ screen.queryByTestId('test-case-Success-area-chart-widget')
+ ).not.toBeInTheDocument();
+
+ if (resolvePromise) {
+ resolvePromise(mockChartData);
+ }
+
+ // After data is fetched, the card widget should appear
+ await waitFor(() => {
+ expect(
+ screen.getByTestId('test-case-Success-area-chart-widget')
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusAreaChartWidget/test-case-status-area-chart-widget.less b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusAreaChartWidget/test-case-status-area-chart-widget.less
new file mode 100644
index 000000000000..e4ec8ae24c5c
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusAreaChartWidget/test-case-status-area-chart-widget.less
@@ -0,0 +1,72 @@
+/*
+ * 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 (reference) '../../../../styles/variables.less';
+
+.test-case-status-icon {
+ height: 40px;
+ width: 40px;
+ border-radius: 50%;
+ border: 6px solid;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 6px;
+
+ color: @grey-900;
+ background-color: @grey-100;
+ border-color: @grey-50;
+
+ svg {
+ height: 20px;
+ width: 20px;
+ }
+ &.failed {
+ color: @red-10;
+ background-color: @alert-error-icon-bg-1;
+ border-color: @red-9;
+ }
+ &.success {
+ color: @green-10;
+ background-color: @green-100;
+ border-color: @green-9;
+ }
+ &.aborted {
+ color: @alert-warning-icon;
+ background-color: @alert-warning-icon-bg-1;
+ border-color: @yellow-10;
+ }
+}
+
+.test-case-area-chart-widget-container {
+ position: relative;
+ background: @grey-26;
+ border-color: @grey-9;
+
+ .test-case-status-widget-footer-divider {
+ border-style: dashed;
+ margin-top: @margin-sm;
+ margin-bottom: @margin-xss;
+ }
+
+ .test-case-status-widget-footer {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+
+ svg {
+ width: 100%;
+ height: auto;
+ max-height: @size-2xl;
+ }
+ }
+}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusPieChartWidget/TestCaseStatusPieChartWidget.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusPieChartWidget/TestCaseStatusPieChartWidget.component.tsx
new file mode 100644
index 000000000000..a8faf00e5684
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusPieChartWidget/TestCaseStatusPieChartWidget.component.tsx
@@ -0,0 +1,127 @@
+/*
+ * 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, Typography } from 'antd';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
+import { ReactComponent as TestCaseIcon } from '../../../../assets/svg/all-activity-v2.svg';
+import {
+ GREEN_3,
+ RED_3,
+ YELLOW_2,
+} from '../../../../constants/Color.constants';
+import { INITIAL_TEST_SUMMARY } from '../../../../constants/TestSuite.constant';
+import { fetchTestCaseSummary } from '../../../../rest/dataQualityDashboardAPI';
+import {
+ getPieChartLabel,
+ getTestCaseTabPath,
+ transformToTestCaseStatusObject,
+} from '../../../../utils/DataQuality/DataQualityUtils';
+import type { CustomPieChartData } from '../../../Visualisations/Chart/Chart.interface';
+import CustomPieChart from '../../../Visualisations/Chart/CustomPieChart.component';
+import { PieChartWidgetCommonProps } from '../../DataQuality.interface';
+import '../chart-widgets.less';
+import { TEST_CASE_STATUS_PIE_SEGMENT_ORDER } from '../ChartWidgets.constants';
+
+const TestCaseStatusPieChartWidget = ({
+ className = '',
+ chartFilter,
+}: PieChartWidgetCommonProps) => {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+
+ const [testCaseSummary, setTestCaseSummary] = useState(INITIAL_TEST_SUMMARY);
+ const [isTestCaseSummaryLoading, setIsTestCaseSummaryLoading] =
+ useState(true);
+
+ const fetchTestSummary = async () => {
+ setIsTestCaseSummaryLoading(true);
+ try {
+ const { data } = await fetchTestCaseSummary(chartFilter);
+ const updatedData = transformToTestCaseStatusObject(data);
+ setTestCaseSummary(updatedData);
+ } catch {
+ setTestCaseSummary(INITIAL_TEST_SUMMARY);
+ } finally {
+ setIsTestCaseSummaryLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchTestSummary();
+ }, [chartFilter]);
+
+ const handleSegmentClick = useCallback(
+ (_entry: CustomPieChartData, index: number) => {
+ const status = TEST_CASE_STATUS_PIE_SEGMENT_ORDER[index];
+ if (status) {
+ navigate(getTestCaseTabPath(status));
+ }
+ },
+ [navigate]
+ );
+
+ const { data, chartLabel } = useMemo(
+ () => ({
+ data: [
+ {
+ name: t('label.success'),
+ value: testCaseSummary.success,
+ color: GREEN_3,
+ },
+ {
+ name: t('label.failed'),
+ value: testCaseSummary.failed,
+ color: RED_3,
+ },
+ {
+ name: t('label.aborted'),
+ value: testCaseSummary.aborted,
+ color: YELLOW_2,
+ },
+ ],
+ chartLabel: getPieChartLabel(
+ t('label.test-plural'),
+ testCaseSummary.total
+ ),
+ }),
+ [testCaseSummary]
+ );
+
+ return (
+
+
+
+
+
+
+
+ {t('label.test-case-result')}
+
+
+
+
+
+ );
+};
+
+export default TestCaseStatusPieChartWidget;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusPieChartWidget/TestCaseStatusPieChartWidget.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusPieChartWidget/TestCaseStatusPieChartWidget.test.tsx
new file mode 100644
index 000000000000..d03ec54d3ccb
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/TestCaseStatusPieChartWidget/TestCaseStatusPieChartWidget.test.tsx
@@ -0,0 +1,199 @@
+/*
+ * 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 '@testing-library/jest-dom/extend-expect';
+import { act, render, screen } from '@testing-library/react';
+import { TestCaseStatus } from '../../../../generated/entity/feed/testCaseResult';
+import { fetchTestCaseSummary } from '../../../../rest/dataQualityDashboardAPI';
+import CustomPieChart from '../../../Visualisations/Chart/CustomPieChart.component';
+import TestCaseStatusPieChartWidget from './TestCaseStatusPieChartWidget.component';
+
+jest.mock('react-router-dom', () => {
+ const actual =
+ jest.requireActual('react-router-dom');
+ const mockNavigate = jest.fn();
+
+ return {
+ ...actual,
+ useNavigate: () => mockNavigate,
+ __getMockNavigate: () => mockNavigate,
+ };
+});
+
+jest.mock('../../../../utils/DataQuality/DataQualityUtils', () => ({
+ transformToTestCaseStatusObject: jest.fn().mockReturnValue({
+ success: 4,
+ failed: 3,
+ aborted: 1,
+ total: 8,
+ }),
+ getPieChartLabel: jest.fn().mockReturnValue(
+ );
+ }
+
+ return (
+
+ {filterBarContent}
+ {chartCards}
+
+ );
+};
+
+export default DataQualityDashboard;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQualityDashboard/DataQualityDashboard.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQualityDashboard/DataQualityDashboard.interface.ts
new file mode 100644
index 000000000000..5a95e85cc2cd
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQualityDashboard/DataQualityDashboard.interface.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2026 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.
+ */
+export type DqDashboardChartFilters = {
+ ownerFqn?: string;
+ glossaryTerms?: string[];
+ tags?: string[];
+ tier?: string[];
+ startTs?: number;
+ endTs?: number;
+ entityFQN?: string;
+ domainFqn?: string;
+};
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQualityDashboard/DataQualityDashboard.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQualityDashboard/DataQualityDashboard.test.tsx
new file mode 100644
index 000000000000..3011b9f185e9
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQualityDashboard/DataQualityDashboard.test.tsx
@@ -0,0 +1,1338 @@
+/*
+ * Copyright 2026 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 { fireEvent, render, screen, waitFor } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import { SearchDropdownOption } from '../../../components/SearchDropdown/SearchDropdown.interface';
+import {
+ ABORTED_CHART_COLOR_SCHEME,
+ FAILED_CHART_COLOR_SCHEME,
+ SUCCESS_CHART_COLOR_SCHEME,
+} from '../../../constants/Chart.constants';
+import { TestCaseStatus } from '../../../generated/tests/testCase';
+import { TestCaseResolutionStatusTypes } from '../../../generated/tests/testCaseResolutionStatus';
+import { DataQualityPageTabs } from '../../../pages/DataQuality/DataQualityPage.interface';
+import { getDataQualityPagePath } from '../../../utils/RouterUtils';
+import { IncidentTimeMetricsType } from '../DataQuality.interface';
+import DataQualityDashboard from './DataQualityDashboard.component';
+
+const mockGetTags = jest.fn().mockResolvedValue({
+ data: [
+ { id: '1', name: 'Tier1', fullyQualifiedName: 'Tier.Tier1' },
+ { id: '2', name: 'Tier2', fullyQualifiedName: 'Tier.Tier2' },
+ ],
+});
+
+const mockSearchQuery = jest.fn().mockResolvedValue({
+ hits: {
+ hits: [
+ {
+ _source: {
+ id: '1',
+ name: 'TestTag',
+ fullyQualifiedName: 'Classification.TestTag',
+ classification: { name: 'Classification' },
+ },
+ },
+ ],
+ },
+});
+
+jest.mock('@openmetadata/ui-core-components', () => ({
+ Button: ({
+ children,
+ className,
+ ...props
+ }: React.PropsWithChildren<{ className?: string }>) => (
+
+ {children}
+
+ ),
+ Card: ({
+ children,
+ className,
+ }: React.PropsWithChildren<{ className?: string }>) => (
+