-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat: migrate Data quality dashboard to open source #26765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1a90fe4
c92454d
8c7bf92
5db2caf
0713ebd
80071ca
ca28e1b
adcf4fa
ce121ee
615907c
d68b8f1
859a5a3
4f2bfd7
e1cb9b6
a77bba6
95bc4aa
7d17106
b83887d
93f5278
3eb8bca
225fc86
f3f1a95
d31ef7a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| 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,140 @@ | ||||||
| /* | ||||||
| * 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 { parseInt } from 'lodash'; | ||||||
| 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] | ||||||
|
||||||
| [dataAssetsCoverageStates] | |
| [dataAssetsCoverageStates, t] |
Copilot
AI
Mar 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When coverageData or totalData is empty, the code sets INITIAL_DATA_ASSETS_COVERAGE_STATES but then still accesses coverageData[0] / totalData[0], resulting in NaN values (or runtime errors). Return immediately after setting the initial state (or guard the parsing logic) so the state remains valid when the API returns no buckets.
| 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'); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This constants file imports
TestCaseStatusfromgenerated/entity/feed/testCaseResult, whileDataQualityDashboardandDataQuality.interface.tsuseTestCaseStatusfromgenerated/tests/testCase. Mixing enum sources for the same concept tends to break typing (enums are not interchangeable) and can cascade into errors in widgets that consume these constants. Standardize on a singleTestCaseStatusenum across the Data Quality dashboard widgets.