Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1a90fe4
feat: Add data dimensions, health, incident metrics, and test case st…
ShaileshParmar11 Mar 25, 2026
c92454d
refactor: Simplify query formatting and clean up JSX structure in Dat…
ShaileshParmar11 Mar 26, 2026
8c7bf92
feat: Add DATA_OBSERVABILITY tab to Domain and Glossary detail pages,…
ShaileshParmar11 Mar 26, 2026
5db2caf
Merge remote-tracking branch 'origin/main' into dq-dashboard-migrate
ShaileshParmar11 Mar 26, 2026
0713ebd
Enhance localization and UI components
ShaileshParmar11 Mar 26, 2026
80071ca
Refactor TagPage layout by wrapping DataQualityDashboard in a div for…
ShaileshParmar11 Mar 26, 2026
ca28e1b
Update localization for "data-dimensions" in multiple languages and r…
ShaileshParmar11 Mar 26, 2026
adcf4fa
Refactor GlossaryTermClassBase tests to use React types for childProps
ShaileshParmar11 Mar 26, 2026
ce121ee
Remove unnecessary newline in DQ_FILTER_KEYS export
ShaileshParmar11 Mar 26, 2026
615907c
Enhance DataQualityDashboard and TagPage tests with new query logic a…
ShaileshParmar11 Mar 26, 2026
d68b8f1
Format expect assertion in TagPage test for improved readability
ShaileshParmar11 Mar 27, 2026
859a5a3
Merge remote-tracking branch 'origin/main' into dq-dashboard-migrate
ShaileshParmar11 Mar 27, 2026
4f2bfd7
address review comment
ShaileshParmar11 Mar 27, 2026
e1cb9b6
move pw test
ShaileshParmar11 Mar 27, 2026
a77bba6
addressing comments
ShaileshParmar11 Mar 27, 2026
95bc4aa
Merge remote-tracking branch 'origin/main' into dq-dashboard-migrate
ShaileshParmar11 Mar 27, 2026
7d17106
minor fix
ShaileshParmar11 Mar 27, 2026
b83887d
pr comment
ShaileshParmar11 Mar 27, 2026
93f5278
prettier
ShaileshParmar11 Mar 27, 2026
3eb8bca
Merge remote-tracking branch 'origin/main' into dq-dashboard-migrate
ShaileshParmar11 Mar 27, 2026
225fc86
Merge branch 'main' into dq-dashboard-migrate
ShaileshParmar11 Mar 27, 2026
f3f1a95
Merge branch 'main' into dq-dashboard-migrate
ShaileshParmar11 Mar 28, 2026
d31ef7a
Merge branch 'main' into dq-dashboard-migrate
ShaileshParmar11 Mar 28, 2026
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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,50 @@
* limitations under the License.
*/
import { expect, Page, Response } from '@playwright/test';
import { SidebarItem } from '../constant/sidebar';
import { TableClass } from '../support/entity/TableClass';
import { redirectToHomePage } from './common';
import { sidebarClick } from './sidebar';

/** Recharts PieChart id for the Test Case Result pie on the Data Quality dashboard. */
export const TEST_CASE_STATUS_PIE_CHART_TEST_ID = 'test-case-result-pie-chart';

/** Recharts PieChart id for the Entity Health Status pie on the Data Quality dashboard. */
export const ENTITY_HEALTH_PIE_CHART_TEST_ID = 'healthy-data-assets-pie-chart';

/** Recharts PieChart id for the Data Assets Coverage pie on the Data Quality dashboard. */
export const DATA_ASSETS_COVERAGE_PIE_CHART_TEST_ID =
'data-assets-coverage-pie-chart';

/**
* Navigate to the Data Quality dashboard (Dashboard sub-tab under Data Quality).
*/
export async function goToDataQualityDashboard(page: Page): Promise<void> {
await redirectToHomePage(page);
const dataQualityReportResponse = page.waitForResponse(
'/api/v1/dataQuality/testSuites/dataQualityReport?q=*'
);
await sidebarClick(page, SidebarItem.DATA_QUALITY);
await page.getByTestId('dashboard').click();
await dataQualityReportResponse;
}

/** Clicks a segment by 0-based index (targets .custom-pie-chart-clickable path). */
export async function clickPieChartSegmentByIndex(
page: Page,
chartTestId: string,
segmentIndex: number
): Promise<void> {
const chart = page.locator(`#${chartTestId}`);
await expect(chart).toBeVisible();
const segmentPath = chart
.locator('.custom-pie-chart-clickable path')
.nth(segmentIndex);
await expect(segmentPath).toBeVisible();
await segmentPath.evaluate((el) => {
el.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
}

export enum ObservabilityFeature {
TEST_CASE = 'Test case',
Expand Down Expand Up @@ -104,7 +147,7 @@
testCaseName: string
) => {
await page.goto(`test-suites/${testSuiteName}`);
await page.waitForSelector('[data-testid="loader"]', {

Check warning on line 150 in openmetadata-ui/src/main/resources/ui/playwright/utils/dataQuality.ts

View workflow job for this annotation

GitHub Actions / lint-playwright

Unexpected use of page.waitForSelector()
state: 'detached',
});
const testCaseResponse = page.waitForResponse(
Expand All @@ -112,7 +155,7 @@
);
await page.click('[data-testid="add-test-case-btn"]');
await testCaseResponse;
await page.waitForSelector(

Check warning on line 158 in openmetadata-ui/src/main/resources/ui/playwright/utils/dataQuality.ts

View workflow job for this annotation

GitHub Actions / lint-playwright

Unexpected use of page.waitForSelector()
"[data-testid='test-case-selection-card'] [data-testid='loader']",
{ state: 'detached' }
);
Expand All @@ -129,7 +172,7 @@
);
await page.click('[data-testid="submit"]');
await updateTestCase;
await page.waitForSelector('[data-testid="test-case-selection-card"]', {

Check warning on line 175 in openmetadata-ui/src/main/resources/ui/playwright/utils/dataQuality.ts

View workflow job for this annotation

GitHub Actions / lint-playwright

Unexpected use of page.waitForSelector()
state: 'detached',
});
};
Expand Down
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,
Comment on lines +14 to +20
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constants file imports TestCaseStatus from generated/entity/feed/testCaseResult, while DataQualityDashboard and DataQuality.interface.ts use TestCaseStatus from generated/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 single TestCaseStatus enum across the Data Quality dashboard widgets.

Copilot uses AI. Check for mistakes.
];

/**
* 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]
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This useMemo uses t(...) for segment labels, but the dependency array only includes dataAssetsCoverageStates. This can produce stale translations and can trip react-hooks/exhaustive-deps. Include t (and any other referenced values that can change) in the dependency array.

Suggested change
[dataAssetsCoverageStates]
[dataAssetsCoverageStates, t]

Copilot uses AI. Check for mistakes.
);

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

return;
}

const covered = parseInt(coverageData[0].originEntityFQN, 10);
let total = parseInt(totalData[0].fullyQualifiedName, 10);

Comment on lines +88 to +96
Copy link

Copilot AI Mar 27, 2026

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.

Copilot uses AI. Check for mistakes.
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