Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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,
];
Original file line number Diff line number Diff line change
@@ -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 (
<Card className={className} loading={isLoading}>
<div className="d-flex flex-column items-center">
<div className="d-flex items-center gap-2">
<div className="custom-chart-icon-background data-assets-coverage-icon icon-container">
<DataAssetsCoverageIcon />
</div>
<Typography.Text className="font-medium text-md">
{t('label.data-asset-plural-coverage')}
</Typography.Text>
</div>
<CustomPieChart
showLegends
data={data}
label={chartLabel}
name="data-assets-coverage"
onSegmentClick={handleSegmentClick}
/>
</div>
</Card>
);
};

export default DataAssetsCoveragePieChartWidget;
Original file line number Diff line number Diff line change
@@ -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<typeof import('react-router-dom')>('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 }) => (
<div>
CustomPieChart.component
<button
data-testid="segment-covered"
onClick={() =>
props.onSegmentClick?.({ name: 'Covered', value: 1 }, 0)
}
/>
<button
data-testid="segment-not-covered"
onClick={() =>
props.onSegmentClick?.({ name: 'Not covered', value: 0 }, 1)
}
/>
</div>
)
)
);

describe('DataAssetsCoveragePieChartWidget', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should render the component', async () => {
render(<DataAssetsCoveragePieChartWidget />);

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(<DataAssetsCoveragePieChartWidget />);

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(<DataAssetsCoveragePieChartWidget chartFilter={filters} />);

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(<DataAssetsCoveragePieChartWidget />);

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(<DataAssetsCoveragePieChartWidget />);

await act(async () => {
await Promise.resolve();
});

const segmentNotCovered = await screen.findByTestId('segment-not-covered');
await act(async () => {
segmentNotCovered.click();
});

expect(mockNavigate).toHaveBeenCalledWith('/explore');
});
});
Loading
Loading