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 ? ( + + + + ) : null; + + const renderFooter = footer || defaultFooter; + + return ( +
+
+
+ +
+
+ + {title} + + + {`${countValue} ${dataLabel ?? ''}`} + +
+
+ {renderFooter && ( + <> + +
{renderFooter}
+ + )} +
+ ); +}; + +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; + }) => ( +
+ Loading... +
+ ), + Typography: ({ + children, + className, + ...props + }: React.PropsWithChildren>) => ( + + {children} + + ), + Button: ({ + children, + ...props + }: React.PropsWithChildren>) => ( + + ), + Divider: ({ className }: { className?: string }) => ( +
+ ), +})); + +const mockIcon = (props: React.SVGProps) => ( + +); + +const mockProps: DataStatisticWidgetProps = { + name: 'test-widget', + title: 'Test Title', + icon: mockIcon, + dataLabel: 'items', + countValue: 10, + redirectPath: '/test-path', + linkLabel: 'View Details', + isLoading: false, +}; + +describe('DataStatisticWidget component', () => { + it('should render the widget with provided props', () => { + render( + + + + ); + + expect( + screen.getByTestId('test-widget-data-statistic-widget') + ).toBeInTheDocument(); + expect(screen.getByText('Test Title')).toBeInTheDocument(); + expect(screen.getByTestId('test-icon')).toBeInTheDocument(); + expect(screen.getByTestId('total-value')).toHaveTextContent('10 items'); + expect(screen.getByText('View Details')).toBeInTheDocument(); + }); + + it('should show loading state when isLoading is true', () => { + render( + + + + ); + + expect(screen.getByTestId('skeleton')).toBeInTheDocument(); + }); + + it('should render with custom icon props', () => { + const iconProps = { width: 24, height: 24, fill: 'red' }; + render( + + + + ); + + const icon = screen.getByTestId('test-icon'); + + expect(icon).toHaveAttribute('width', '24'); + expect(icon).toHaveAttribute('height', '24'); + expect(icon).toHaveAttribute('fill', 'red'); + }); + + it('should apply styleType class to icon container', () => { + const styleType: Lowercase = 'success'; + render( + + + + ); + + const iconContainer = screen.getByTestId('test-icon').parentElement; + + expect(iconContainer).toHaveClass('data-statistic-widget-icon', 'success'); + }); + + it('should render correct link with redirectPath', () => { + render( + + + + ); + + const link = screen.getByRole('link'); + + expect(link).toHaveAttribute('href', '/test-path'); + }); + + it('should render link with correct label', () => { + render( + + + + ); + + const link = screen.getByRole('link'); + + expect(link).toHaveTextContent('View Details'); + }); + + it('should handle large count values correctly', () => { + render( + + + + ); + + expect(screen.getByTestId('total-value')).toHaveTextContent('999999 items'); + }); + + it('should handle zero count value', () => { + render( + + + + ); + + expect(screen.getByTestId('total-value')).toHaveTextContent('0 items'); + }); + + it('should handle empty dataLabel', () => { + render( + + + + ); + + // When dataLabel is empty, there's no trailing space + expect(screen.getByTestId('total-value')).toHaveTextContent('10'); + }); + + it('should render link with proper navigation', () => { + const { container } = render( + + + + ); + + const link = container.querySelector('a[href="/test-path"]'); + + expect(link).toBeInTheDocument(); + expect(link).toHaveTextContent('View Details'); + }); + + it('should render without styleType prop', () => { + render( + + + + ); + + const iconContainer = screen.getByTestId('test-icon').parentElement; + + expect(iconContainer).toHaveClass('data-statistic-widget-icon'); + expect(iconContainer).not.toHaveClass('success'); + expect(iconContainer).not.toHaveClass('failed'); + }); + + it('should render with different styleType values', () => { + const styleTypes: Array> = [ + 'success', + 'failed', + 'aborted', + ]; + + styleTypes.forEach((styleType) => { + const { unmount } = render( + + + + ); + + const iconContainer = screen.getByTestId('test-icon').parentElement; + + expect(iconContainer).toHaveClass( + 'data-statistic-widget-icon', + styleType + ); + + unmount(); + }); + }); + + it('should maintain proper structure', () => { + const { container } = render( + + + + ); + + const contentWrapper = container.querySelector( + '.tw\\:flex.tw\\:gap-4.tw\\:items-center.tw\\:mb-1' + ); + + expect(contentWrapper).toBeInTheDocument(); + + const titleSection = container.querySelector( + '.data-statistic-widget-title' + ); + + expect(titleSection).toBeInTheDocument(); + }); + + it('should render typography elements correctly', () => { + render( + + + + ); + + // Check title element exists + const titleElement = screen.getByText('Test Title'); + + expect(titleElement).toBeInTheDocument(); + + // Check value element + const valueParagraph = screen.getByTestId('total-value'); + + expect(valueParagraph).toBeInTheDocument(); + expect(valueParagraph).toHaveTextContent('10 items'); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/DataStatisticWidget/data-statistic-widget.less b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/DataStatisticWidget/data-statistic-widget.less new file mode 100644 index 000000000000..de529cc00826 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/DataStatisticWidget/data-statistic-widget.less @@ -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 (reference) '../../../../styles/variables.less'; + +.data-statistic-widget-title { + div&.ant-typography { + margin-bottom: @margin-lg; + } +} + +// Default styles for title +.data-statistic-title-default { + color: @grey-600; + font-weight: 500; + margin-bottom: @size-xxs; +} + +// Default styles for count value +.data-statistic-count-default { + color: @text-color; + font-weight: 600; + font-size: @size-lg; +} + +.data-statistic-widget-icon { + height: 40px; + width: 40px; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + color: @grey-900; + border-color: @grey-50; + + svg { + height: 40px; + width: 40px; + } + + // When status classes are present, apply circular bordered style (for data observability) + &.failed, + &.success, + &.aborted { + border-radius: 50%; + border: 6px solid; + padding: 6px; + background-color: @grey-100; + border-color: @grey-50; + + svg { + height: 20px; + width: 20px; + } + } + + // Status-specific colors for circular bordered style + &.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; + } +} + +.data-statistic-widget-box { + padding: @padding-md @padding-md @padding-xs @padding-md; + border-radius: @border-rad-sm; + border: 1px solid @grey-200; + + .data-statistic-widget-divider { + height: 0; + background: transparent; + border: 0; + border-top: 1px dashed @grey-200; + margin-top: @margin-sm; + margin-bottom: @margin-xss; + } +} + +.data-statistic-widget-box .data-statistic-widget-default-button { + padding: 0; + color: @primary-7; + font-weight: 500; + font-size: @size-sm; + + &:hover, + &:focus, + &:active { + color: @primary-7; + background-color: transparent; + } + + svg { + width: @size-lg; + height: 10px; + } +} + +.data-statistic-widget-link { + display: flex; + align-items: flex-end; + height: 100%; + + a:hover { + text-decoration: none; + } + + .ant-btn { + display: flex; + align-items: center; + gap: @size-xs; + border: none; + + &.failed { + color: @red-10; + background-color: @red-9; + &:hover { + background-color: @alert-error-icon-bg-1; + } + } + &.success { + color: @green-10; + background-color: @green-9; + &:hover { + background-color: @green-100; + } + } + &.aborted { + color: @alert-warning-icon; + background-color: @yellow-10; + &:hover { + background-color: @alert-warning-icon-bg-1; + } + } + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/EntityHealthStatusPieChartWidget/EntityHealthStatusPieChartWidget.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/EntityHealthStatusPieChartWidget/EntityHealthStatusPieChartWidget.component.tsx new file mode 100644 index 000000000000..a21ca5f4d52f --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/EntityHealthStatusPieChartWidget/EntityHealthStatusPieChartWidget.component.tsx @@ -0,0 +1,128 @@ +/* + * 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 HealthCheckIcon } from '../../../../assets/svg/ic-green-heart-border.svg'; +import { GREEN_3, RED_3 } from '../../../../constants/Color.constants'; +import { INITIAL_ENTITY_HEALTH_MATRIX } from '../../../../constants/profiler.constant'; +import { fetchEntityCoveredWithDQ } from '../../../../rest/dataQualityDashboardAPI'; +import { + getPieChartLabel, + getTestCaseTabPath, +} 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 { BINARY_STATUS_PIE_SEGMENT_ORDER } from '../ChartWidgets.constants'; + +const EntityHealthStatusPieChartWidget = ({ + className = '', + chartFilter, +}: PieChartWidgetCommonProps) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(true); + const [entityHealthStates, setEntityHealthStates] = useState<{ + healthy: number; + unhealthy: number; + total: number; + }>(INITIAL_ENTITY_HEALTH_MATRIX); + + const handleSegmentClick = useCallback( + (_entry: CustomPieChartData, index: number) => { + const status = BINARY_STATUS_PIE_SEGMENT_ORDER[index]; + if (status) { + navigate(getTestCaseTabPath(status)); + } + }, + [navigate] + ); + + const { data, chartLabel } = useMemo( + () => ({ + data: [ + { + name: t('label.healthy'), + value: entityHealthStates.healthy, + color: GREEN_3, + }, + { + name: t('label.unhealthy'), + value: entityHealthStates.unhealthy, + color: RED_3, + }, + ], + chartLabel: getPieChartLabel( + t('label.entity-plural'), + entityHealthStates.total + ), + }), + [entityHealthStates] + ); + + const fetchEntityHealthSummary = async () => { + setIsLoading(true); + try { + const { data: unhealthyData } = await fetchEntityCoveredWithDQ( + chartFilter, + true + ); + const { data: totalData } = await fetchEntityCoveredWithDQ( + chartFilter, + false + ); + if (unhealthyData.length === 0 || totalData.length === 0) { + setEntityHealthStates(INITIAL_ENTITY_HEALTH_MATRIX); + } + const unhealthy = parseInt(unhealthyData[0].originEntityFQN); + const total = parseInt(totalData[0].originEntityFQN); + + setEntityHealthStates({ unhealthy, healthy: total - unhealthy, total }); + } catch { + setEntityHealthStates(INITIAL_ENTITY_HEALTH_MATRIX); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchEntityHealthSummary(); + }, [chartFilter]); + + return ( + +
+
+
+ +
+ + {t('label.healthy-data-asset-plural')} + +
+ +
+
+ ); +}; + +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(
Test Label
), + getTestCaseTabPath: jest.fn((status: TestCaseStatus) => ({ + pathname: '/data-quality/test-cases', + search: `testCaseStatus=${status}`, + })), +})); + +jest.mock('../../../Visualisations/Chart/CustomPieChart.component', () => + jest + .fn() + .mockImplementation( + (props: { onSegmentClick?: (e: unknown, i: number) => void }) => ( +
+ CustomPieChart.component +
+ ) + ) +); + +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 = ( + +
+ + + {statusData.title === DataQualityDimensions.NoDimension + ? t('label.no-dimension') + : statusData.title} + +
+
+ + {statusData.total} + +
+ {Object.entries(countCard).map(([key, value]) => ( + +
+ {value} +
+
+ ))} +
+
+
+ ); + + 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 ( + <> +
+ {showIcon && ( +
+ +
+ )} +
+ + {title} + + + {latestValue} + +
+
+ + {showEmptyFooter ? ( + +
+ {footerWhenEmpty} +
+
+ ) : ( + + )} + + ); + }, [ + title, + chartData, + name, + chartColorScheme, + height, + footerWhenEmpty, + isChartLoading, + t, + testCaseStatus, + showIcon, + ]); + + const getTestCaseStatusMetrics = async () => { + setIsChartLoading(true); + try { + const { data } = await fetchTestCaseStatusMetricsByDays( + testCaseStatus, + chartFilter + ); + const updatedData = data.map((cur) => { + return { + timestamp: +cur.timestamp, + count: +cur['testCase.fullyQualifiedName'], + }; + }); + + setChartData(updatedData); + } catch { + setChartData([]); + } finally { + setIsChartLoading(false); + } + }; + + useEffect(() => { + getTestCaseStatusMetrics(); + }, [chartFilter]); + + const containerClassName = classNames( + 'test-case-area-chart-widget-container tw:p-4', + className, + { 'chart-widget-link-no-underline': !isUndefined(redirectPath) } + ); + + if (isChartLoading) { + return ; + } + + return ( +
+
+ {redirectPath ? ( + {bodyElement} + ) : ( + bodyElement + )} +
+
+ ); +}; + +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) => ( +
+
CustomAreaChart
+
{props.name}
+
{props.height}
+
+ {JSON.stringify(props.colorScheme)} +
+
{JSON.stringify(props.data)}
+
+ )) +); + +// Mock SVG icons +jest.mock('../../../../assets/svg/ic-check.svg', () => ({ + ReactComponent: (props: React.SVGProps) => ( + + ), +})); + +jest.mock('../../../../assets/svg/ic-warning-2.svg', () => ({ + ReactComponent: (props: React.SVGProps) => ( + + ), +})); + +jest.mock('@openmetadata/ui-core-components', () => ({ + Skeleton: ({ + width, + height, + }: { + width?: string | number; + height?: number; + }) => ( +
+ Loading... +
+ ), + Tooltip: ({ + children, + title, + }: { + children: React.ReactNode; + title?: string; + }) =>
{children}
, + 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(
Test Label
), + getTestCaseTabPath: jest.fn((status: TestCaseStatus) => ({ + pathname: '/data-quality/test-cases', + search: `testCaseStatus=${status}`, + })), +})); + +jest.mock('../../../../constants/TestSuite.constant', () => ({ + INITIAL_TEST_SUMMARY: { + success: 0, + failed: 0, + aborted: 0, + total: 0, + }, +})); +jest.mock('../../../Visualisations/Chart/CustomPieChart.component', () => + jest + .fn() + .mockImplementation( + (props: { onSegmentClick?: (e: unknown, i: number) => void }) => ( +
+ CustomPieChart.component +
+ ) + ) +); + +jest.mock('../../../../rest/dataQualityDashboardAPI', () => ({ + fetchTestCaseSummary: jest.fn().mockImplementation(() => + Promise.resolve({ + data: [ + { document_count: '4', 'testCaseResult.testCaseStatus': 'success' }, + { document_count: '3', 'testCaseResult.testCaseStatus': 'failed' }, + { document_count: '1', 'testCaseResult.testCaseStatus': 'aborted' }, + ], + }) + ), +})); + +describe('TestCaseStatusPieChartWidget', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render the component', async () => { + render(); + + expect( + await screen.findByText('label.test-case-result') + ).toBeInTheDocument(); + expect( + await screen.findByText('CustomPieChart.component') + ).toBeInTheDocument(); + }); + + it('fetchTestCaseSummary should be called', async () => { + render(); + + await act(async () => { + await Promise.resolve(); + }); + + expect(fetchTestCaseSummary).toHaveBeenCalledTimes(1); + }); + + it('fetchTestCaseSummary 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(fetchTestCaseSummary).toHaveBeenCalledWith(filters); + }); + + 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}`, + }); + + getTestCaseTabPath.mockClear(); + mockNavigate.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}`, + }); + + getTestCaseTabPath.mockClear(); + mockNavigate.mockClear(); + + const segment2 = await screen.findByTestId('segment-2'); + await act(async () => { + segment2.click(); + }); + + expect(getTestCaseTabPath).toHaveBeenCalledWith(TestCaseStatus.Aborted); + expect(mockNavigate).toHaveBeenCalledWith({ + pathname: '/data-quality/test-cases', + search: `testCaseStatus=${TestCaseStatus.Aborted}`, + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/chart-widgets.less b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/chart-widgets.less new file mode 100644 index 000000000000..dede8f086a7f --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/ChartWidgets/chart-widgets.less @@ -0,0 +1,75 @@ +/* + * 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'; + +.chart-total-count-value-link { + &.ant-typography a { + font-size: 1.25rem /* 20px */; + line-height: 1.75rem /* 28px */; + font-weight: 500; + } +} + +.chart-widget-link-no-underline { + a:hover { + text-decoration: none; + } + + &:hover .chart-widget-link-underline { + text-decoration: underline; + } +} + +.custom-chart-background.ant-card { + background: @grey-26; + border-color: @grey-9; + + .custom-tooltip-area-chart.ant-card { + background-color: @white; + } +} + +.custom-chart-icon-background { + height: 32px; + width: 32px; + border-radius: 50%; + background-color: @grey-2; + display: flex; + align-items: center; + justify-content: center; + + svg { + height: 16px; + width: 16px; + } + + &.data-assets-coverage-icon, + &.all-tests-icon { + &.icon-container { + background-color: @blue-13; + svg { + color: @blue-500; + } + } + } + + &.health-check-icon { + &.icon-container { + background-color: @green-12; + svg { + color: @green-500; + } + } + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQuality.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQuality.interface.ts index 40157b930119..094f6c590efb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQuality.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQuality.interface.ts @@ -1,13 +1,3 @@ -import { DateRangeObject } from 'Models'; -import { SVGAttributes } from 'react'; -import { LinkProps } from 'react-router-dom'; -import { TestCaseType } from '../../enums/TestSuite.enum'; -import { TestCaseStatus } from '../../generated/tests/testCase'; -import { TestCaseResolutionStatusTypes } from '../../generated/tests/testCaseResolutionStatus'; -import { TestPlatform } from '../../generated/tests/testDefinition'; -import { DataQualityDashboardChartFilters } from '../../pages/DataQuality/DataQualityPage.interface'; -import { AreaChartColorScheme } from '../Visualisations/Chart/Chart.interface'; - /* * Copyright 2023 Collate. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +11,16 @@ import { AreaChartColorScheme } from '../Visualisations/Chart/Chart.interface'; * limitations under the License. */ +import { DateRangeObject } from 'Models'; +import { SVGAttributes } from 'react'; +import { LinkProps } from 'react-router-dom'; +import { TestCaseType } from '../../enums/TestSuite.enum'; +import { TestCaseStatus } from '../../generated/tests/testCase'; +import { TestCaseResolutionStatusTypes } from '../../generated/tests/testCaseResolutionStatus'; +import { TestPlatform } from '../../generated/tests/testDefinition'; +import { DataQualityDashboardChartFilters } from '../../pages/DataQuality/DataQualityPage.interface'; +import { AreaChartColorScheme } from '../Visualisations/Chart/Chart.interface'; + export enum IncidentTimeMetricsType { TIME_TO_RESPONSE = 'timeToResponse', TIME_TO_RESOLUTION = 'timeToResolution', @@ -59,6 +59,7 @@ export interface IncidentTypeAreaChartWidgetProps { name: string; chartFilter?: DataQualityDashboardChartFilters; redirectPath?: LinkProps['to']; + height?: number; } export interface IncidentTimeChartWidgetProps { @@ -69,6 +70,7 @@ export interface IncidentTimeChartWidgetProps { height?: number; redirectPath?: LinkProps['to']; } + export interface TestCaseStatusAreaChartWidgetProps { title: string; testCaseStatus: TestCaseStatus; @@ -77,6 +79,9 @@ export interface TestCaseStatusAreaChartWidgetProps { chartFilter?: DataQualityDashboardChartFilters; height?: number; redirectPath?: LinkProps['to']; + showIcon?: boolean; + className?: string; + footerWhenEmpty?: React.ReactNode; } export interface PieChartWidgetCommonProps { @@ -86,12 +91,17 @@ export interface PieChartWidgetCommonProps { export interface DataStatisticWidgetProps { name: string; - title: string; + title: string | React.ReactNode; icon: SvgComponent; - dataLabel: string; - countValue: number; - redirectPath: LinkProps['to']; - linkLabel: string; + dataLabel?: string; + countValue: number | string; + redirectPath?: LinkProps['to']; + linkLabel?: string; + footer?: React.ReactNode; isLoading?: boolean; iconProps?: SVGAttributes; + styleType?: Lowercase; + titleClassName?: string; + countValueClassName?: string; + className?: string; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQualityDashboard/DataQualityDashboard.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQualityDashboard/DataQualityDashboard.component.tsx new file mode 100644 index 000000000000..31a3acfc6178 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQualityDashboard/DataQualityDashboard.component.tsx @@ -0,0 +1,820 @@ +/* + * 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, + Grid, + Tooltip, + TooltipTrigger, +} from '@openmetadata/ui-core-components'; +import classNames from 'classnames'; +import { isEmpty, isEqual, omit, uniqBy } from 'lodash'; +import { DateRangeObject } from 'Models'; +import QueryString from 'qs'; +import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as DropDownIcon } from '../../../assets/svg/drop-down.svg'; +import DatePickerMenu from '../../../components/common/DatePickerMenu/DatePickerMenu.component'; +import { UserTeamSelectableList } from '../../../components/common/UserTeamSelectableList/UserTeamSelectableList.component'; +import PageHeader from '../../../components/PageHeader/PageHeader.component'; +import SearchDropdown from '../../../components/SearchDropdown/SearchDropdown'; +import { SearchDropdownOption } from '../../../components/SearchDropdown/SearchDropdown.interface'; +import { WILD_CARD_CHAR } from '../../../constants/char.constants'; +import { + ABORTED_CHART_COLOR_SCHEME, + FAILED_CHART_COLOR_SCHEME, + SUCCESS_CHART_COLOR_SCHEME, +} from '../../../constants/Chart.constants'; +import { PAGE_SIZE_BASE, ROUTES } from '../../../constants/constants'; +import { + DATA_QUALITY_DASHBOARD_HEADER, + DQ_FILTER_KEYS, +} from '../../../constants/DataQuality.constants'; +import { PROFILER_FILTER_RANGE } from '../../../constants/profiler.constant'; +import { SearchIndex } from '../../../enums/search.enum'; +import { Tag } from '../../../generated/entity/classification/tag'; +import { TestCaseStatus } from '../../../generated/tests/testCase'; +import { TestCaseResolutionStatusTypes } from '../../../generated/tests/testCaseResolutionStatus'; +import { EntityReference } from '../../../generated/type/entityReference'; +import { DataQualityPageTabs } from '../../../pages/DataQuality/DataQualityPage.interface'; +import { searchQuery } from '../../../rest/searchAPI'; +import { getTags } from '../../../rest/tagAPI'; +import { getSelectedOptionLabelString } from '../../../utils/AdvancedSearchUtils'; +import { + formatDate, + getCurrentMillis, + getEndOfDayInMillis, + getEpochMillisForPastDays, + getStartOfDayInMillis, +} from '../../../utils/date-time/DateTimeUtils'; +import { getEntityName } from '../../../utils/EntityUtils'; +import { getDataQualityPagePath } from '../../../utils/RouterUtils'; +import DataAssetsCoveragePieChartWidget from '../ChartWidgets/DataAssetsCoveragePieChartWidget/DataAssetsCoveragePieChartWidget.component'; +import EntityHealthStatusPieChartWidget from '../ChartWidgets/EntityHealthStatusPieChartWidget/EntityHealthStatusPieChartWidget.component'; +import IncidentTimeChartWidget from '../ChartWidgets/IncidentTimeChartWidget/IncidentTimeChartWidget.component'; +import IncidentTypeAreaChartWidget from '../ChartWidgets/IncidentTypeAreaChartWidget/IncidentTypeAreaChartWidget.component'; +import StatusByDimensionCardWidget from '../ChartWidgets/StatusByDimensionCardWidget/StatusByDimensionCardWidget.component'; +import TestCaseStatusAreaChartWidget from '../ChartWidgets/TestCaseStatusAreaChartWidget/TestCaseStatusAreaChartWidget.component'; +import TestCaseStatusPieChartWidget from '../ChartWidgets/TestCaseStatusPieChartWidget/TestCaseStatusPieChartWidget.component'; +import { IncidentTimeMetricsType } from '../DataQuality.interface'; +import './data-quality-dashboard.style.less'; +import { DqDashboardChartFilters } from './DataQualityDashboard.interface'; + +const DataQualityDashboard = ({ + initialFilters, + hideFilterBar = false, + hiddenFilters = [], + isGovernanceView = false, + className, +}: { + initialFilters?: DqDashboardChartFilters; + hideFilterBar?: boolean; + hiddenFilters?: Array<'owner' | 'tier' | 'tags' | 'glossaryTerms'>; + isGovernanceView?: boolean; + className?: string; +}) => { + const { t } = useTranslation(); + + const { dataHealth, dataDimensions, testCasesStatus, incidentMetrics } = + DATA_QUALITY_DASHBOARD_HEADER; + + const translatedHeaders = useMemo( + () => ({ + dataHealth: { + header: t(dataHealth.header), + subHeader: t(dataHealth.subHeader), + }, + dataDimensions: { + header: t(dataDimensions.header), + subHeader: t(dataDimensions.subHeader), + }, + testCasesStatus: { + header: t(testCasesStatus.header), + subHeader: t(testCasesStatus.subHeader), + }, + incidentMetrics: { + header: t(incidentMetrics.header), + subHeader: t(incidentMetrics.subHeader), + }, + }), + [t, dataHealth, dataDimensions, testCasesStatus, incidentMetrics] + ); + + const DEFAULT_RANGE_DATA = useMemo(() => { + return { + startTs: getStartOfDayInMillis( + getEpochMillisForPastDays(PROFILER_FILTER_RANGE.last30days.days) + ), + endTs: getEndOfDayInMillis(getCurrentMillis()), + key: 'last30days', + }; + }, []); + + const [glossaryTermOptions, setGlossaryTermOptions] = useState<{ + defaultOptions: SearchDropdownOption[]; + options: SearchDropdownOption[]; + }>({ + defaultOptions: [], + options: [], + }); + const [isGlossaryTermLoading, setIsGlossaryTermLoading] = useState(false); + + const [tagOptions, setTagOptions] = useState<{ + defaultOptions: SearchDropdownOption[]; + options: SearchDropdownOption[]; + }>({ + defaultOptions: [], + options: [], + }); + const [isTagLoading, setIsTagLoading] = useState(false); + const [selectedTagFilter, setSelectedTagFilter] = useState< + SearchDropdownOption[] + >([]); + const [selectedGlossaryTermFilter, setSelectedGlossaryTermFilter] = useState< + SearchDropdownOption[] + >([]); + const [selectedTierFilter, setSelectedTierFilter] = useState< + SearchDropdownOption[] + >([]); + const [selectedOwnerFilter, setSelectedOwnerFilter] = + useState(); + + const [dateRangeObject, setDateRangeObject] = + useState(DEFAULT_RANGE_DATA); + const [tier, setTier] = useState<{ + tags: Tag[]; + isLoading: boolean; + options: SearchDropdownOption[]; + }>({ + tags: [], + isLoading: true, + options: [], + }); + const [chartFilter, setChartFilter] = useState({ + startTs: DEFAULT_RANGE_DATA.startTs, + endTs: DEFAULT_RANGE_DATA.endTs, + ...initialFilters, + }); + + const defaultFilters = useMemo(() => { + const tags = [ + ...(chartFilter.tags ?? []), + ...(chartFilter.glossaryTerms ?? []), + ]; + + return { + ...omit(chartFilter, 'glossaryTerms'), + tags: isEmpty(tags) ? undefined : tags, + }; + }, [chartFilter]); + + const pieChartFilters = useMemo(() => { + return { + ownerFqn: defaultFilters.ownerFqn, + tags: defaultFilters.tags, + tier: defaultFilters.tier, + startTs: defaultFilters.startTs, + endTs: defaultFilters.endTs, + domainFqn: defaultFilters.domainFqn, + }; + }, [ + defaultFilters.ownerFqn, + defaultFilters.tier, + defaultFilters.tags, + defaultFilters.startTs, + defaultFilters.endTs, + defaultFilters.domainFqn, + ]); + + const selectedOwnerKeys = useMemo(() => { + return ( + selectedOwnerFilter?.map((owner) => ({ + key: owner.id, + label: getEntityName(owner), + })) ?? [] + ); + }, [selectedOwnerFilter]); + + const defaultTierOptions = useMemo(() => { + return tier.tags.map((op) => ({ + key: op.fullyQualifiedName ?? op.name, + label: getEntityName(op), + })); + }, [tier]); + + const handleTierChange = (tiers: SearchDropdownOption[] = []) => { + setSelectedTierFilter(tiers); + setChartFilter((prev) => ({ + ...prev, + tier: tiers.map((tag) => tag.key), + })); + }; + + const handleDateRangeChange = (value: DateRangeObject) => { + if (!isEqual(value, dateRangeObject)) { + const dateRange = { + startTs: getStartOfDayInMillis(value.startTs), + endTs: getEndOfDayInMillis(value.endTs), + }; + setDateRangeObject(dateRange); + + setChartFilter((prev) => ({ + ...prev, + ...dateRange, + })); + } + }; + + const handleTagChange = (tags: SearchDropdownOption[] = []) => { + setSelectedTagFilter(tags); + setChartFilter((prev) => ({ + ...prev, + tags: tags.map((tag) => tag.key), + })); + }; + + const fetchTagOptions = async (query = WILD_CARD_CHAR) => { + const response = await searchQuery({ + searchIndex: SearchIndex.TAG, + query: query === WILD_CARD_CHAR ? query : `*${query}*`, + filters: 'disabled:false AND !classification.name:Tier', + pageSize: PAGE_SIZE_BASE, + }); + const hits = response.hits.hits; + const tagFilterOptions = hits.map((hit) => { + const source = hit._source; + + return { + key: source.fullyQualifiedName ?? source.name, + label: `${source.classification?.name}.${source.name}`, + }; + }); + + return tagFilterOptions; + }; + + const handleTagSearch = async (query: string) => { + if (isEmpty(query)) { + setTagOptions((prev) => ({ + ...prev, + options: prev.defaultOptions, + })); + } else { + setIsTagLoading(true); + try { + const response = await fetchTagOptions(query); + setTagOptions((prev) => ({ + ...prev, + options: response, + })); + } catch { + // we will not show the toast error message for suggestion API + } finally { + setIsTagLoading(false); + } + } + }; + + const handleGlossaryTermChange = ( + glossaryTerms: SearchDropdownOption[] = [] + ) => { + setSelectedGlossaryTermFilter(glossaryTerms); + setChartFilter((prev) => ({ + ...prev, + glossaryTerms: glossaryTerms.map((term) => term.key), + })); + }; + + const fetchGlossaryTermOptions = async (query = WILD_CARD_CHAR) => { + const response = await searchQuery({ + searchIndex: SearchIndex.GLOSSARY_TERM, + query: query === WILD_CARD_CHAR ? query : `*${query}*`, + pageSize: PAGE_SIZE_BASE, + }); + const hits = response.hits.hits; + const glossaryTermFilterOptions = hits.map((hit) => { + const source = hit._source; + + return { + key: source.fullyQualifiedName ?? source.name, + label: source.fullyQualifiedName ?? source.name, + }; + }); + + return glossaryTermFilterOptions; + }; + + const handleGlossaryTermSearch = async (query: string) => { + if (isEmpty(query)) { + setGlossaryTermOptions((prev) => ({ + ...prev, + options: prev.defaultOptions, + })); + } else { + setIsGlossaryTermLoading(true); + try { + const response = await fetchGlossaryTermOptions(query); + setGlossaryTermOptions((prev) => ({ + ...prev, + options: response, + })); + } catch { + // we will not show the toast error message for suggestion API + } finally { + setIsGlossaryTermLoading(false); + } + } + }; + + const handleTierSearch = async (query: string) => { + if (query) { + setTier((prev) => ({ + ...prev, + options: prev.options.filter( + (value) => + value.label + .toLocaleLowerCase() + .includes(query.toLocaleLowerCase()) || + value.key.toLocaleLowerCase().includes(query.toLocaleLowerCase()) + ), + })); + } else { + setTier((prev) => ({ + ...prev, + options: defaultTierOptions, + })); + } + }; + + const fetchDefaultTagOptions = async () => { + if (tagOptions.defaultOptions.length) { + setTagOptions((prev) => ({ + ...prev, + options: [...selectedTagFilter, ...prev.defaultOptions], + })); + + return; + } + + try { + setIsTagLoading(true); + const response = await fetchTagOptions(); + setTagOptions((prev) => ({ + ...prev, + defaultOptions: response, + options: response, + })); + } catch { + // we will not show the toast error message for search API + } finally { + setIsTagLoading(false); + } + }; + + const fetchDefaultGlossaryTermOptions = async () => { + if (glossaryTermOptions.defaultOptions.length) { + setGlossaryTermOptions((prev) => ({ + ...prev, + options: [...selectedGlossaryTermFilter, ...prev.defaultOptions], + })); + + return; + } + + try { + setIsGlossaryTermLoading(true); + const response = await fetchGlossaryTermOptions(); + setGlossaryTermOptions((prev) => ({ + ...prev, + defaultOptions: response, + options: response, + })); + } catch { + // we will not show the toast error message for search API + } finally { + setIsGlossaryTermLoading(false); + } + }; + + const getTierTag = async () => { + setTier((prev) => ({ ...prev, isLoading: true })); + try { + const { data } = await getTags({ + parent: 'Tier', + }); + + setTier((prev) => ({ + ...prev, + tags: data, + options: data.map((op) => ({ + key: op.fullyQualifiedName ?? op.name, + label: getEntityName(op), + })), + })); + } catch { + // error + } finally { + setTier((prev) => ({ ...prev, isLoading: false })); + } + }; + + const fetchDefaultTierOptions = () => { + setTier((prev) => ({ + ...prev, + options: defaultTierOptions, + })); + }; + + const handleOwnerChange = (owners: EntityReference[] = []) => { + setSelectedOwnerFilter(owners); + setChartFilter((prev) => ({ + ...prev, + ownerFqn: isEmpty(owners) + ? undefined + : owners[0].name ?? owners[0].fullyQualifiedName, + })); + }; + + // Stable string derived from the array so the effect doesn't re-run on every + // render due to a new array reference (e.g. hiddenFilters={['tags']} literal). + const hiddenFiltersKey = hiddenFilters.join(','); + + const showOwnerFilter = !hiddenFilters.includes(DQ_FILTER_KEYS.OWNER); + const showTierFilter = !hiddenFilters.includes(DQ_FILTER_KEYS.TIER); + const showTagsFilter = !hiddenFilters.includes(DQ_FILTER_KEYS.TAGS); + const showGlossaryTermsFilter = !hiddenFilters.includes( + DQ_FILTER_KEYS.GLOSSARY_TERMS + ); + + useEffect(() => { + if (hideFilterBar) { + return; + } + if (showTierFilter) { + getTierTag(); + } + if (showTagsFilter) { + fetchDefaultTagOptions(); + } + if (showGlossaryTermsFilter) { + fetchDefaultGlossaryTermOptions(); + } + }, [hideFilterBar, hiddenFiltersKey]); + + const tags = useMemo( + () => ({ + options: uniqBy(tagOptions.options, 'key'), + selectedKeys: selectedTagFilter, + onChange: handleTagChange, + onGetInitialOptions: fetchDefaultTagOptions, + onSearch: handleTagSearch, + isSuggestionsLoading: isTagLoading, + }), + [isTagLoading, tagOptions, selectedTagFilter] + ); + + const glossaryTerms = useMemo( + () => ({ + options: uniqBy(glossaryTermOptions.options, 'key'), + selectedKeys: selectedGlossaryTermFilter, + onChange: handleGlossaryTermChange, + onGetInitialOptions: fetchDefaultGlossaryTermOptions, + onSearch: handleGlossaryTermSearch, + isSuggestionsLoading: isGlossaryTermLoading, + }), + [ + isGlossaryTermLoading, + glossaryTermOptions, + selectedGlossaryTermFilter, + handleGlossaryTermChange, + ] + ); + + const tierFilter = useMemo( + () => ({ + options: tier.options, + selectedKeys: selectedTierFilter, + onChange: handleTierChange, + onGetInitialOptions: fetchDefaultTierOptions, + onSearch: handleTierSearch, + isSuggestionsLoading: tier.isLoading, + }), + [selectedTierFilter, tier] + ); + + const showFilterBar = !hideFilterBar; + const hasVisibleFilters = + showOwnerFilter || + showTierFilter || + showTagsFilter || + showGlossaryTermsFilter; + + const cardClassName = classNames('data-quality-dashboard-card-section', { + 'tw:ring-0': isGovernanceView, + 'tw:shadow-none': isGovernanceView, + }); + + const cardBodyClass = isGovernanceView ? 'tw:py-6' : 'tw:p-6'; + + const filterBarContent = ( +
+ {showFilterBar && hasVisibleFilters && ( +
+ {showOwnerFilter && ( + + + +
0 + ? getSelectedOptionLabelString(selectedOwnerKeys, true) + : undefined + }> +
+ {t('label.owner')} + {selectedOwnerKeys.length > 0 && ( + + {': '} + + {getSelectedOptionLabelString(selectedOwnerKeys)} + + + )} +
+ +
+
+
+
+ )} + + {showTierFilter && ( + + )} + + {showTagsFilter && ( + + )} + + {showGlossaryTermsFilter && ( + + )} +
+ )} +
+ + {`${formatDate(chartFilter.startTs, true)} - ${formatDate( + chartFilter.endTs, + true + )}`} + + +
+
+ ); + + const chartCards = ( + <> + + +
+ + + + + + + + + + + + +
+
+
+ + + +
+ + +
+
+
+ + + +
+ + + + + + + + + + + + +
+
+
+ + + +
+ + + + + + + + + + + + + + + +
+
+
+ + ); + + if (isGovernanceView) { + return ( +
+
+ {filterBarContent} +
+
+ {chartCards} +
+
+ ); + } + + 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 }>) => ( + + ), + Card: ({ + children, + className, + }: React.PropsWithChildren<{ className?: string }>) => ( +
{children}
+ ), + Grid: Object.assign( + ({ + children, + className, + ...props + }: React.PropsWithChildren<{ className?: string }>) => ( +
+ {children} +
+ ), + { + Item: ({ + children, + className, + ...props + }: React.PropsWithChildren<{ className?: string }>) => ( +
+ {children} +
+ ), + } + ), + Tooltip: ({ children }: React.PropsWithChildren>) => ( + <>{children} + ), + TooltipTrigger: ({ + children, + }: React.PropsWithChildren>) => <>{children}, +})); + +jest.mock('../../../utils/RouterUtils', () => ({ + getDataQualityPagePath: jest.fn((tab: string) => `/data-quality/${tab}`), +})); + +jest.mock('../../../components/PageHeader/PageHeader.component', () => + jest.fn().mockImplementation(() =>
) +); + +jest.mock('../../../rest/tagAPI', () => ({ + getTags: () => mockGetTags(), +})); + +jest.mock('../../../rest/searchAPI', () => ({ + searchQuery: (...args: unknown[]) => mockSearchQuery(...args), +})); + +jest.mock( + '../../../components/common/DatePickerMenu/DatePickerMenu.component', + () => + jest.fn().mockImplementation(({ handleDateRangeChange }) => ( + + )) +); + +jest.mock( + '../../../components/common/UserTeamSelectableList/UserTeamSelectableList.component', + () => ({ + UserTeamSelectableList: jest + .fn() + .mockImplementation(({ onUpdate, children }) => ( +
+ {children} + +
+ )), + }) +); + +jest.mock('../../../components/SearchDropdown/SearchDropdown', () => + jest + .fn() + .mockImplementation(({ label, onChange, onSearch, selectedKeys }) => ( +
+ + {onSearch && ( + + )} + {selectedKeys + .map((option: SearchDropdownOption) => option.label) + .join(', ')} +
+ )) +); +jest.mock('../../../utils/AdvancedSearchUtils', () => { + return { + getSelectedOptionLabelString: jest + .fn() + .mockImplementation((selectedOptions) => + selectedOptions + .map((option: SearchDropdownOption) => option.label) + .join(', ') + ), + }; +}); + +jest.mock( + '../ChartWidgets/TestCaseStatusPieChartWidget/TestCaseStatusPieChartWidget.component', + () => + jest + .fn() + .mockImplementation(() => ( +
+ TestCaseStatusPieChartWidget +
+ )) +); + +jest.mock( + '../ChartWidgets/EntityHealthStatusPieChartWidget/EntityHealthStatusPieChartWidget.component', + () => + jest + .fn() + .mockImplementation(() => ( +
+ EntityHealthStatusPieChartWidget +
+ )) +); + +jest.mock( + '../ChartWidgets/DataAssetsCoveragePieChartWidget/DataAssetsCoveragePieChartWidget.component', + () => + jest + .fn() + .mockImplementation(() => ( +
+ DataAssetsCoveragePieChartWidget +
+ )) +); + +jest.mock( + '../ChartWidgets/StatusByDimensionCardWidget/StatusByDimensionCardWidget.component', + () => + jest + .fn() + .mockImplementation(() => ( +
+ StatusByDimensionCardWidget +
+ )) +); + +// Mock chart widgets to capture props for testing +const mockTestCaseStatusAreaChartWidget = jest.fn(); +const mockIncidentTypeAreaChartWidget = jest.fn(); +const mockIncidentTimeChartWidget = jest.fn(); + +jest.mock( + '../ChartWidgets/TestCaseStatusAreaChartWidget/TestCaseStatusAreaChartWidget.component', + () => + jest.fn().mockImplementation((props) => { + mockTestCaseStatusAreaChartWidget(props); + + return ( +
+ TestCaseStatusAreaChartWidget {props.name} +
+ ); + }) +); + +jest.mock( + '../ChartWidgets/IncidentTypeAreaChartWidget/IncidentTypeAreaChartWidget.component', + () => + jest.fn().mockImplementation((props) => { + mockIncidentTypeAreaChartWidget(props); + + return ( +
+ IncidentTypeAreaChartWidget {props.name} +
+ ); + }) +); + +jest.mock( + '../ChartWidgets/IncidentTimeChartWidget/IncidentTimeChartWidget.component', + () => + jest.fn().mockImplementation((props) => { + mockIncidentTimeChartWidget(props); + + return ( +
+ IncidentTimeChartWidget {props.name} +
+ ); + }) +); + +// Pie chart widgets capture props so we can assert filter wiring +const mockDataAssetsCoveragePieChartWidget = jest.fn(); +const mockEntityHealthStatusPieChartWidget = jest.fn(); +const mockTestCaseStatusPieChartWidget = jest.fn(); + +// Re-mock pie chart widgets to also capture props +jest.mock( + '../ChartWidgets/TestCaseStatusPieChartWidget/TestCaseStatusPieChartWidget.component', + () => + jest.fn().mockImplementation((props) => { + mockTestCaseStatusPieChartWidget(props); + + return ( +
+ TestCaseStatusPieChartWidget +
+ ); + }) +); + +jest.mock( + '../ChartWidgets/EntityHealthStatusPieChartWidget/EntityHealthStatusPieChartWidget.component', + () => + jest.fn().mockImplementation((props) => { + mockEntityHealthStatusPieChartWidget(props); + + return ( +
+ EntityHealthStatusPieChartWidget +
+ ); + }) +); + +jest.mock( + '../ChartWidgets/DataAssetsCoveragePieChartWidget/DataAssetsCoveragePieChartWidget.component', + () => + jest.fn().mockImplementation((props) => { + mockDataAssetsCoveragePieChartWidget(props); + + return ( +
+ DataAssetsCoveragePieChartWidget +
+ ); + }) +); + +describe('DataQualityDashboard', () => { + it('should render the DataQualityDashboard component', async () => { + render(, { wrapper: MemoryRouter }); + + expect(screen.getByTestId('dq-dashboard-container')).toBeInTheDocument(); + expect(screen.getByTestId('user-team-selectable-list')).toBeInTheDocument(); + expect( + screen.getByTestId('search-dropdown-label.tier') + ).toBeInTheDocument(); + expect(screen.getByTestId('search-dropdown-label.tag')).toBeInTheDocument(); + expect( + screen.getByTestId('search-dropdown-label.glossary-term') + ).toBeInTheDocument(); + expect(screen.getByTestId('date-picker-menu')).toBeInTheDocument(); + expect( + screen.getByTestId('test-case-status-pie-chart-widget') + ).toBeInTheDocument(); + expect( + screen.getByTestId('entity-health-status-pie-chart-widget') + ).toBeInTheDocument(); + expect( + screen.getByTestId('data-assets-coverage-pie-chart-widget') + ).toBeInTheDocument(); + expect( + screen.getByTestId('status-by-dimension-card-widget') + ).toBeInTheDocument(); + expect( + screen.getByTestId('test-case-status-area-chart-widget-success') + ).toBeInTheDocument(); + expect( + screen.getByTestId('test-case-status-area-chart-widget-aborted') + ).toBeInTheDocument(); + expect( + screen.getByTestId('test-case-status-area-chart-widget-failed') + ).toBeInTheDocument(); + expect( + screen.getByTestId('incident-type-area-chart-widget-open-incident') + ).toBeInTheDocument(); + expect( + screen.getByTestId('incident-type-area-chart-widget-resolved-incident') + ).toBeInTheDocument(); + expect( + screen.getByTestId('incident-time-chart-widget-response-time') + ).toBeInTheDocument(); + expect( + screen.getByTestId('incident-time-chart-widget-resolution-time') + ).toBeInTheDocument(); + }); + + it('should handle owner filter change', async () => { + render(, { wrapper: MemoryRouter }); + + fireEvent.click(screen.getByTestId('user-team-selectable-list')); + + await waitFor(() => { + expect(screen.getByText('owner1')).toBeInTheDocument(); + }); + }); + + it('should handle tier filter change', async () => { + render(, { wrapper: MemoryRouter }); + + fireEvent.click(screen.getByTestId('search-dropdown-label.tier')); + + await waitFor(() => { + expect(screen.getByText('Tag 1')).toBeInTheDocument(); + }); + }); + + it('should handle date range change', async () => { + render(, { wrapper: MemoryRouter }); + + fireEvent.click(screen.getByTestId('date-picker-menu')); + + await waitFor(() => { + expect(screen.getByText('DatePickerMenu')).toBeInTheDocument(); + }); + }); + + describe('Chart Widget Props and Configuration', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should pass correct color schemes to TestCaseStatusAreaChartWidget components', async () => { + render(, { wrapper: MemoryRouter }); + + await waitFor(() => { + expect(mockTestCaseStatusAreaChartWidget).toHaveBeenCalled(); + }); + + // Verify success widget gets success color scheme + expect(mockTestCaseStatusAreaChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + chartColorScheme: SUCCESS_CHART_COLOR_SCHEME, + testCaseStatus: TestCaseStatus.Success, + name: 'success', + title: 'label.success', + }) + ); + + // Verify aborted widget gets aborted color scheme + expect(mockTestCaseStatusAreaChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + chartColorScheme: ABORTED_CHART_COLOR_SCHEME, + testCaseStatus: TestCaseStatus.Aborted, + name: 'aborted', + title: 'label.aborted', + }) + ); + + // Verify failed widget gets failed color scheme + expect(mockTestCaseStatusAreaChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + chartColorScheme: FAILED_CHART_COLOR_SCHEME, + testCaseStatus: TestCaseStatus.Failed, + name: 'failed', + title: 'label.failed', + }) + ); + }); + + it('should pass correct redirect paths to TestCaseStatusAreaChartWidget components', async () => { + render(, { wrapper: MemoryRouter }); + + await waitFor(() => { + expect(mockTestCaseStatusAreaChartWidget).toHaveBeenCalled(); + }); + + // Check success widget redirect path + expect(mockTestCaseStatusAreaChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + redirectPath: { + pathname: getDataQualityPagePath(DataQualityPageTabs.TEST_CASES), + search: 'testCaseStatus=Success', + }, + }) + ); + + // Check failed widget redirect path + expect(mockTestCaseStatusAreaChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + redirectPath: { + pathname: getDataQualityPagePath(DataQualityPageTabs.TEST_CASES), + search: 'testCaseStatus=Failed', + }, + }) + ); + }); + + it('should pass correct props to IncidentTypeAreaChartWidget components', async () => { + render(, { wrapper: MemoryRouter }); + + await waitFor(() => { + expect(mockIncidentTypeAreaChartWidget).toHaveBeenCalled(); + }); + + // Check open incident widget + expect(mockIncidentTypeAreaChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + incidentStatusType: TestCaseResolutionStatusTypes.New, + name: 'open-incident', + title: 'label.open-incident-plural', + redirectPath: expect.objectContaining({ + pathname: '/incident-manager', + search: expect.stringContaining('testCaseResolutionStatusType=New'), + }), + }) + ); + + // Check resolved incident widget + expect(mockIncidentTypeAreaChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + incidentStatusType: TestCaseResolutionStatusTypes.Resolved, + name: 'resolved-incident', + title: 'label.resolved-incident-plural', + redirectPath: expect.objectContaining({ + pathname: '/incident-manager', + search: expect.stringContaining( + 'testCaseResolutionStatusType=Resolved' + ), + }), + }) + ); + }); + + it('should pass correct props to IncidentTimeChartWidget components', async () => { + render(, { wrapper: MemoryRouter }); + + await waitFor(() => { + expect(mockIncidentTimeChartWidget).toHaveBeenCalled(); + }); + + // Check response time widget + expect(mockIncidentTimeChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + incidentMetricType: IncidentTimeMetricsType.TIME_TO_RESPONSE, + name: 'response-time', + title: 'label.response-time', + }) + ); + + // Check resolution time widget + expect(mockIncidentTimeChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + incidentMetricType: IncidentTimeMetricsType.TIME_TO_RESOLUTION, + name: 'resolution-time', + title: 'label.resolution-time', + }) + ); + }); + }); + + describe('Filter State Management', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should handle tag filter changes and update chart filters', async () => { + render(, { wrapper: MemoryRouter }); + + fireEvent.click(screen.getByTestId('search-dropdown-label.tag')); + + await waitFor(() => { + // Verify that all chart widgets receive the updated filters + expect(mockTestCaseStatusAreaChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + chartFilter: expect.objectContaining({ + tags: ['tag1'], + }), + }) + ); + }); + }); + + it('should handle glossary term filter changes', async () => { + render(, { wrapper: MemoryRouter }); + + fireEvent.click( + screen.getByTestId('search-dropdown-label.glossary-term') + ); + + await waitFor(() => { + // Verify glossary terms are included in tag filters for widgets + expect(mockTestCaseStatusAreaChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + chartFilter: expect.objectContaining({ + tags: ['tag1'], + }), + }) + ); + }); + }); + + it('should merge tag and glossary term filters correctly', async () => { + render(, { wrapper: MemoryRouter }); + + // Apply both tag and glossary term filters + fireEvent.click(screen.getByTestId('search-dropdown-label.tag')); + fireEvent.click( + screen.getByTestId('search-dropdown-label.glossary-term') + ); + + await waitFor(() => { + // Should merge both filter types into tags array + expect(mockTestCaseStatusAreaChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + chartFilter: expect.objectContaining({ + tags: expect.arrayContaining(['tag1']), + }), + }) + ); + }); + }); + + it('should handle owner filter changes', async () => { + render(, { wrapper: MemoryRouter }); + + fireEvent.click(screen.getByTestId('user-team-selectable-list')); + + await waitFor(() => { + expect(mockTestCaseStatusAreaChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + chartFilter: expect.objectContaining({ + ownerFqn: 'owner1', + }), + }) + ); + }); + }); + }); + + describe('API Integration and Error Handling', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should handle component mount successfully', async () => { + render(, { wrapper: MemoryRouter }); + + // Component should render successfully despite any API loading states + await waitFor(() => { + expect( + screen.getByTestId('dq-dashboard-container') + ).toBeInTheDocument(); + }); + }); + + it('should handle API errors gracefully for tag fetching', async () => { + mockGetTags.mockRejectedValueOnce(new Error('API Error')); + + render(, { wrapper: MemoryRouter }); + + // Component should still render despite API error + await waitFor(() => { + expect( + screen.getByTestId('dq-dashboard-container') + ).toBeInTheDocument(); + }); + }); + + it('should handle search API errors gracefully', async () => { + mockSearchQuery.mockRejectedValueOnce(new Error('Search API Error')); + + render(, { wrapper: MemoryRouter }); + + // Component should still render despite search API error + await waitFor(() => { + expect( + screen.getByTestId('dq-dashboard-container') + ).toBeInTheDocument(); + }); + }); + }); + + describe('Dashboard Structure and Layout', () => { + it('should render dashboard sections with correct export classes', async () => { + render(, { wrapper: MemoryRouter }); + + const exportContainers = document.querySelectorAll( + '.export-pdf-container' + ); + + expect(exportContainers).toHaveLength(4); // Data Health, Dimensions, Test Cases, Incidents + }); + + it('should render dashboard sections with correct card classes', async () => { + render(, { wrapper: MemoryRouter }); + + const cardSections = document.querySelectorAll( + '.data-quality-dashboard-card-section' + ); + + expect(cardSections).toHaveLength(4); + }); + + it('should render grid layout correctly', async () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + const mainContainer = container.querySelector( + '.m-b-md[data-testid="dq-dashboard-container"]' + ); + + expect(mainContainer).toBeInTheDocument(); + + // Four chart card sections rendered inside the grid + const sections = container.querySelectorAll('.export-pdf-container'); + + expect(sections).toHaveLength(4); + }); + }); + + describe('Date Range Handling', () => { + it('should initialize with default 30-day range', async () => { + render(, { wrapper: MemoryRouter }); + + await waitFor(() => { + // Check that widgets receive chart filters with proper date range + expect(mockTestCaseStatusAreaChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + chartFilter: expect.objectContaining({ + startTs: expect.any(Number), + endTs: expect.any(Number), + }), + }) + ); + }); + }); + + it('should update chart filters when date range changes', async () => { + render(, { wrapper: MemoryRouter }); + + fireEvent.click(screen.getByTestId('date-picker-menu')); + + await waitFor(() => { + // Should trigger re-render with new date range + expect(mockTestCaseStatusAreaChartWidget).toHaveBeenCalled(); + }); + }); + }); + + describe('User Interface Elements', () => { + it('should display date range UI elements', async () => { + render(, { wrapper: MemoryRouter }); + + // Should show date picker component + await waitFor(() => { + expect(screen.getByTestId('date-picker-menu')).toBeInTheDocument(); + }); + }); + + it('should show selected filter values in dropdown buttons', async () => { + render(, { wrapper: MemoryRouter }); + + fireEvent.click(screen.getByTestId('user-team-selectable-list')); + + await waitFor(() => { + expect(screen.getByText('owner1')).toBeInTheDocument(); + }); + }); + }); + + describe('hideFilterBar prop', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('hides Owner, Tier, Tag and Glossary Term dropdowns when hideFilterBar is true', async () => { + render(, { wrapper: MemoryRouter }); + + expect( + screen.queryByTestId('user-team-selectable-list') + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId('search-dropdown-label.tier') + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId('search-dropdown-label.tag') + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId('search-dropdown-label.glossary-term') + ).not.toBeInTheDocument(); + }); + + it('still renders the date picker when hideFilterBar is true', async () => { + render(, { wrapper: MemoryRouter }); + + expect(screen.getByTestId('date-picker-menu')).toBeInTheDocument(); + }); + + it('still renders all chart widgets when hideFilterBar is true', async () => { + render(, { wrapper: MemoryRouter }); + + expect( + screen.getByTestId('test-case-status-pie-chart-widget') + ).toBeInTheDocument(); + expect( + screen.getByTestId('entity-health-status-pie-chart-widget') + ).toBeInTheDocument(); + expect( + screen.getByTestId('data-assets-coverage-pie-chart-widget') + ).toBeInTheDocument(); + }); + + it('does not call tier/tag/glossary APIs when hideFilterBar is true', async () => { + render(, { wrapper: MemoryRouter }); + + await waitFor(() => { + expect( + screen.getByTestId('dq-dashboard-container') + ).toBeInTheDocument(); + }); + + expect(mockGetTags).not.toHaveBeenCalled(); + expect(mockSearchQuery).not.toHaveBeenCalled(); + }); + + it('does not call APIs when hideFilterBar is true even with hiddenFilters set', async () => { + render(, { + wrapper: MemoryRouter, + }); + + await waitFor(() => { + expect( + screen.getByTestId('dq-dashboard-container') + ).toBeInTheDocument(); + }); + + expect(mockGetTags).not.toHaveBeenCalled(); + expect(mockSearchQuery).not.toHaveBeenCalled(); + }); + + it('calls tier/tag/glossary APIs when hideFilterBar is false (default)', async () => { + render(, { wrapper: MemoryRouter }); + + await waitFor(() => { + expect(mockGetTags).toHaveBeenCalled(); + expect(mockSearchQuery).toHaveBeenCalled(); + }); + }); + }); + + describe('initialFilters prop', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('passes initialFilters.tags to pie chart widgets', async () => { + render( + , + { wrapper: MemoryRouter } + ); + + await waitFor(() => { + expect(mockTestCaseStatusPieChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + chartFilter: expect.objectContaining({ + tags: ['PII.Sensitive'], + }), + }) + ); + }); + }); + + it('passes initialFilters.domainFqn to pie chart widgets', async () => { + render( + , + { wrapper: MemoryRouter } + ); + + await waitFor(() => { + expect(mockTestCaseStatusPieChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + chartFilter: expect.objectContaining({ + domainFqn: 'Engineering', + }), + }) + ); + }); + }); + + it('passes initialFilters.glossaryTerms merged into tags for pie chart widgets', async () => { + render( + , + { wrapper: MemoryRouter } + ); + + await waitFor(() => { + expect(mockTestCaseStatusPieChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + chartFilter: expect.objectContaining({ + tags: ['Finance.Revenue'], + }), + }) + ); + }); + }); + + it('merges initialFilters.tags and glossaryTerms into a single tags array', async () => { + render( + , + { wrapper: MemoryRouter } + ); + + await waitFor(() => { + expect(mockTestCaseStatusPieChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + chartFilter: expect.objectContaining({ + tags: expect.arrayContaining([ + 'PII.Sensitive', + 'Finance.Revenue', + ]), + }), + }) + ); + }); + }); + + it('passes initialFilters.ownerFqn to pie chart widgets', async () => { + render( + , + { wrapper: MemoryRouter } + ); + + await waitFor(() => { + expect(mockTestCaseStatusPieChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + chartFilter: expect.objectContaining({ + ownerFqn: 'john.doe', + }), + }) + ); + }); + }); + }); + + describe('className prop', () => { + it('applies custom className to the root container', () => { + const { container } = render( + , + { wrapper: MemoryRouter } + ); + + expect( + container.querySelector( + '.my-custom-class[data-testid="dq-dashboard-container"]' + ) + ).toBeInTheDocument(); + }); + + it('always includes m-b-md class on the root container', () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + expect( + container.querySelector('.m-b-md[data-testid="dq-dashboard-container"]') + ).toBeInTheDocument(); + }); + }); + + describe('hiddenFilters prop', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('hides only the tags dropdown when hiddenFilters includes tags', () => { + render(, { + wrapper: MemoryRouter, + }); + + expect( + screen.queryByTestId('search-dropdown-label.tag') + ).not.toBeInTheDocument(); + expect( + screen.getByTestId('user-team-selectable-list') + ).toBeInTheDocument(); + expect( + screen.getByTestId('search-dropdown-label.tier') + ).toBeInTheDocument(); + expect( + screen.getByTestId('search-dropdown-label.glossary-term') + ).toBeInTheDocument(); + }); + + it('hides only the glossary term dropdown when hiddenFilters includes glossaryTerms', () => { + render(, { + wrapper: MemoryRouter, + }); + + expect( + screen.queryByTestId('search-dropdown-label.glossary-term') + ).not.toBeInTheDocument(); + expect( + screen.getByTestId('user-team-selectable-list') + ).toBeInTheDocument(); + expect( + screen.getByTestId('search-dropdown-label.tier') + ).toBeInTheDocument(); + expect( + screen.getByTestId('search-dropdown-label.tag') + ).toBeInTheDocument(); + }); + + it('hides only the tier dropdown when hiddenFilters includes tier', () => { + render(, { + wrapper: MemoryRouter, + }); + + expect( + screen.queryByTestId('search-dropdown-label.tier') + ).not.toBeInTheDocument(); + expect( + screen.getByTestId('user-team-selectable-list') + ).toBeInTheDocument(); + expect( + screen.getByTestId('search-dropdown-label.tag') + ).toBeInTheDocument(); + expect( + screen.getByTestId('search-dropdown-label.glossary-term') + ).toBeInTheDocument(); + }); + + it('hides only the owner filter when hiddenFilters includes owner', () => { + render(, { + wrapper: MemoryRouter, + }); + + expect( + screen.queryByTestId('user-team-selectable-list') + ).not.toBeInTheDocument(); + expect( + screen.getByTestId('search-dropdown-label.tier') + ).toBeInTheDocument(); + expect( + screen.getByTestId('search-dropdown-label.tag') + ).toBeInTheDocument(); + expect( + screen.getByTestId('search-dropdown-label.glossary-term') + ).toBeInTheDocument(); + }); + + it('does not call getTags API when tier is in hiddenFilters', async () => { + render(, { + wrapper: MemoryRouter, + }); + + await waitFor(() => { + expect( + screen.getByTestId('dq-dashboard-container') + ).toBeInTheDocument(); + }); + + expect(mockGetTags).not.toHaveBeenCalled(); + }); + + it('does not call tag search API when tags is in hiddenFilters', async () => { + render(, { + wrapper: MemoryRouter, + }); + + await waitFor(() => { + expect( + screen.getByTestId('dq-dashboard-container') + ).toBeInTheDocument(); + }); + + // only glossaryTerms fetch runs — searchQuery called once, not twice + expect(mockSearchQuery).toHaveBeenCalledTimes(1); + }); + + it('does not call glossary term search API when glossaryTerms is in hiddenFilters', async () => { + render(, { + wrapper: MemoryRouter, + }); + + await waitFor(() => { + expect( + screen.getByTestId('dq-dashboard-container') + ).toBeInTheDocument(); + }); + + // only tags fetch runs — searchQuery called once, not twice + expect(mockSearchQuery).toHaveBeenCalledTimes(1); + }); + + it('hideFilterBar is a hard override — hides the entire bar even when hiddenFilters is set', () => { + render(, { + wrapper: MemoryRouter, + }); + + // hideFilterBar=true is a hard override: entire filter bar is hidden regardless of hiddenFilters + expect( + screen.queryByTestId('user-team-selectable-list') + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId('search-dropdown-label.tier') + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId('search-dropdown-label.glossary-term') + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId('search-dropdown-label.tag') + ).not.toBeInTheDocument(); + }); + }); + + describe('isGovernanceView prop', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders governance layout structure instead of Row layout', () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + expect( + container.querySelector('.data-quality-governance-layout') + ).toBeInTheDocument(); + expect( + container.querySelector('.data-quality-governance-filter-bar') + ).toBeInTheDocument(); + expect( + container.querySelector('.data-quality-governance-charts') + ).toBeInTheDocument(); + }); + + it('does not use Row layout m-b-md class in governance view', () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + expect( + container.querySelector('.m-b-md[data-testid="dq-dashboard-container"]') + ).not.toBeInTheDocument(); + }); + + it('applies dq-dashboard-container testid to governance layout div', () => { + render(, { + wrapper: MemoryRouter, + }); + + expect(screen.getByTestId('dq-dashboard-container')).toBeInTheDocument(); + }); + + it('applies borderless styling to all 4 card sections in governance view', () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + expect( + container.querySelectorAll(String.raw`.tw\:ring-0.tw\:shadow-none`) + ).toHaveLength(4); + }); + + it('does not apply borderless styling in default view', () => { + const { container } = render(, { + wrapper: MemoryRouter, + }); + + expect( + container.querySelector(String.raw`.tw\:ring-0.tw\:shadow-none`) + ).not.toBeInTheDocument(); + }); + + it('renders all chart widgets inside governance charts area', () => { + render(, { + wrapper: MemoryRouter, + }); + + expect( + screen.getByTestId('test-case-status-pie-chart-widget') + ).toBeInTheDocument(); + expect( + screen.getByTestId('entity-health-status-pie-chart-widget') + ).toBeInTheDocument(); + expect( + screen.getByTestId('data-assets-coverage-pie-chart-widget') + ).toBeInTheDocument(); + expect( + screen.getByTestId('status-by-dimension-card-widget') + ).toBeInTheDocument(); + }); + + it('applies custom className to governance layout root', () => { + const { container } = render( + , + { wrapper: MemoryRouter } + ); + + expect( + container.querySelector( + '.custom-governance.data-quality-governance-layout' + ) + ).toBeInTheDocument(); + }); + + it('combines isGovernanceView with hiddenFilters correctly', () => { + const { container } = render( + , + { wrapper: MemoryRouter } + ); + + expect( + container.querySelector('.data-quality-governance-layout') + ).toBeInTheDocument(); + expect( + screen.queryByTestId('search-dropdown-label.tag') + ).not.toBeInTheDocument(); + expect( + screen.getByTestId('search-dropdown-label.tier') + ).toBeInTheDocument(); + }); + + it('passes initialFilters to chart widgets in governance view', async () => { + render( + , + { wrapper: MemoryRouter } + ); + + await waitFor(() => { + expect(mockTestCaseStatusPieChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + chartFilter: expect.objectContaining({ + tags: ['PII.Sensitive'], + }), + }) + ); + }); + }); + }); + + describe('glossaryTerms to tags merge (defaultFilters)', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('glossaryTerms filter change merges into tags for area chart widgets', async () => { + render(, { wrapper: MemoryRouter }); + + fireEvent.click( + screen.getByTestId('search-dropdown-label.glossary-term') + ); + + await waitFor(() => { + expect(mockTestCaseStatusAreaChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + chartFilter: expect.objectContaining({ + tags: ['tag1'], + }), + }) + ); + // glossaryTerms key itself is omitted from defaultFilters + expect(mockTestCaseStatusAreaChartWidget).toHaveBeenCalledWith( + expect.not.objectContaining({ + chartFilter: expect.objectContaining({ + glossaryTerms: expect.anything(), + }), + }) + ); + }); + }); + + it('empty tags and glossaryTerms produce undefined tags in defaultFilters', async () => { + render(, { wrapper: MemoryRouter }); + + await waitFor(() => { + expect(mockTestCaseStatusAreaChartWidget).toHaveBeenCalledWith( + expect.objectContaining({ + chartFilter: expect.not.objectContaining({ + tags: expect.anything(), + }), + }) + ); + }); + }); + }); + + describe('wildcard query fix in fetchTagOptions and fetchGlossaryTermOptions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls searchQuery with bare wildcard (*) on mount — not triple-star (***)', async () => { + render(, { wrapper: MemoryRouter }); + + await waitFor(() => { + expect(mockSearchQuery).toHaveBeenCalled(); + }); + + const wildcardCalls = mockSearchQuery.mock.calls.filter( + (args: unknown[]) => (args[0] as Record).query === '*' + ); + const tripleStarCalls = mockSearchQuery.mock.calls.filter( + (args: unknown[]) => + (args[0] as Record).query === '***' + ); + + expect(wildcardCalls.length).toBeGreaterThanOrEqual(2); // tags + glossaryTerms + expect(tripleStarCalls).toHaveLength(0); + }); + + it('calls searchQuery with *text* when tag search text is non-empty', async () => { + render(, { wrapper: MemoryRouter }); + + fireEvent.click(screen.getByTestId('search-dropdown-search-label.tag')); + + await waitFor(() => { + const wrappedCalls = mockSearchQuery.mock.calls.filter( + (args: unknown[]) => + (args[0] as Record).query === '*pii*' + ); + + expect(wrappedCalls.length).toBeGreaterThanOrEqual(1); + }); + }); + + it('calls searchQuery with *text* when glossary term search text is non-empty', async () => { + render(, { wrapper: MemoryRouter }); + + fireEvent.click( + screen.getByTestId('search-dropdown-search-label.glossary-term') + ); + + await waitFor(() => { + const wrappedCalls = mockSearchQuery.mock.calls.filter( + (args: unknown[]) => + (args[0] as Record).query === '*pii*' + ); + + expect(wrappedCalls.length).toBeGreaterThanOrEqual(1); + }); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQualityDashboard/data-quality-dashboard.style.less b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQualityDashboard/data-quality-dashboard.style.less new file mode 100644 index 000000000000..47a19d663206 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/DataQualityDashboard/data-quality-dashboard.style.less @@ -0,0 +1,51 @@ +/* + * 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 (reference) '../../../styles/variables.less'; + +.data-quality-dashboard-card-section { + .page-header-container { + margin-bottom: @margin-mlg; + } + + .data-quality-dashboard-pie-chart { + background-color: @grey-26; + border-color: @grey-9; + } +} + +.data-quality-governance-tab-wrapper { + background-color: @white; + padding: @padding-md; + border-radius: @border-rad-sm; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.data-quality-governance-layout { + display: flex; + flex-direction: column; + height: 100%; + + .data-quality-governance-filter-bar { + flex-shrink: 0; + padding-bottom: @padding-sm; + } + + .data-quality-governance-charts { + flex: 1; + overflow-y: auto; + min-height: 0; + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.test.tsx index c6db906e5ea9..5eeafc0f0113 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.test.tsx @@ -164,7 +164,7 @@ describe('Test Glossary-term component', () => { const tabs = await screen.findAllByRole('tab'); - expect(tabs).toHaveLength(6); + expect(tabs).toHaveLength(7); expect(tabs[0].textContent).toBe('label.overview'); tabs @@ -189,7 +189,7 @@ describe('Test Glossary-term component', () => { expect(await screen.findByText('GlossaryTermTab')).toBeInTheDocument(); - expect(tabs).toHaveLength(6); + expect(tabs).toHaveLength(7); expect(tabs.map((tab) => tab.textContent)).toStrictEqual([ 'label.overview', 'label.glossary-term-plural2', @@ -197,6 +197,7 @@ describe('Test Glossary-term component', () => { 'label.activity-feed-and-task-plural0', 'label.relations-graph', 'label.custom-property-plural', + 'label.data-observability', ]); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/DataQuality.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/DataQuality.constants.ts index d55295735bfa..76b67748aa9d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/DataQuality.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/DataQuality.constants.ts @@ -13,6 +13,8 @@ import { ReactComponent as SkippedIcon } from '../assets/svg/ic-aborted.svg'; import { ReactComponent as FailedIcon } from '../assets/svg/ic-fail.svg'; import { ReactComponent as SuccessIcon } from '../assets/svg/ic-successful.svg'; +import { StatusData } from '../components/DataQuality/ChartWidgets/StatusCardWidget/StatusCardWidget.interface'; +import { DataQualityDimensions } from '../generated/tests/testDefinition'; export const TEST_CASE_STATUS_ICON = { Aborted: SkippedIcon, @@ -20,3 +22,63 @@ export const TEST_CASE_STATUS_ICON = { Queued: SkippedIcon, Success: SuccessIcon, }; + +const moveItemsToEnd = (arr: T[], predicate: (item: T) => boolean): T[] => { + const keep: T[] = []; + const move: T[] = []; + for (const item of arr) { + if (predicate(item)) { + move.push(item); + } else { + keep.push(item); + } + } + + return [...keep, ...move]; +}; + +export const NO_DIMENSION = 'No Dimension'; +export const DIMENSIONS_DATA = moveItemsToEnd( + Object.values(DataQualityDimensions), + (dimension: DataQualityDimensions) => + dimension === DataQualityDimensions.NoDimension +); + +export const DEFAULT_DIMENSIONS_DATA = DIMENSIONS_DATA.reduce((acc, item) => { + return { + ...acc, + [item]: { + title: item, + success: 0, + failed: 0, + aborted: 0, + total: 0, + }, + }; +}, {} as { [key: string]: StatusData }); + +export const DATA_QUALITY_DASHBOARD_HEADER = { + dataHealth: { + header: 'label.data-health', + subHeader: 'message.data-health-sub-header', + }, + dataDimensions: { + header: 'label.data-dimensions', + subHeader: 'message.data-dimensions-sub-header', + }, + testCasesStatus: { + header: 'label.test-case-status', + subHeader: 'message.test-case-status-sub-header', + }, + incidentMetrics: { + header: 'label.incident-metrics', + subHeader: 'message.incident-metrics-sub-header', + }, +}; + +export const DQ_FILTER_KEYS = { + OWNER: 'owner', + TIER: 'tier', + TAGS: 'tags', + GLOSSARY_TERMS: 'glossaryTerms', +} as const; diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json index c956ce23abb0..e376cd849641 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json @@ -451,10 +451,13 @@ "data-contract-plural": "عقود البيانات", "data-contract-status": "حالة عقد البيانات", "data-count-plural": "أعداد البيانات", + "data-dimensions": "أبعاد البيانات", "data-discovery": "اكتشاف البيانات", "data-distribution": "توزيع البيانات", "data-entity": "بيانات {{entity}}", + "data-health": "صحة البيانات", "data-health-by-entity": "سلامة البيانات حسب {{entity}}", + "data-health-overview": "Data health overview", "data-insight": "رؤية البيانات", "data-insight-active-user-summary": "المستخدمون الأكثر نشاطًا", "data-insight-chart": "مخطط رؤية البيانات", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "الإعلانات غير النشطة", "incident": "حادثة", "incident-manager": "مدير الحوادث", + "incident-metrics": "مقاييس الحوادث", "incident-plural": "حوادث", "incident-status": "حالة الحادثة", "include": "تضمين", @@ -1999,6 +2003,7 @@ "test-case-plural": "حالات اختبار", "test-case-resolution-status": "حالة قرار حالة الاختبار", "test-case-result": "نتائج حالة الاختبار", + "test-case-status": "حالة حالات الاختبار", "test-definition": "Test Definition", "test-definition-plural": "Test Definitions", "test-email": "اختبار البريد الإلكتروني", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "توقعات سياسة الأمان والوصول", "data-contract-sla-description": "توقعات اتفاقية مستوى الخدمة", "data-contract-terms-of-service-description": "القواعد التي توافق عليها عند استخدام خدمة", + "data-dimensions-sub-header": "احصل على رؤى حول أبعاد جودة البيانات الرئيسية—مثل الدقة والاتساق والنزاهة—لتقييم اكتمال وموثوقية بياناتك.", + "data-health-sub-header": "تتبع التغطية والجودة ونتائج الاختبار لأصول البيانات الخاصة بك لمراقبة الصحة العامة للبيانات وتحديد مجالات التحسين.", "data-insight-alert-destination-description": "إرسال إشعارات البريد الإلكتروني إلى المسؤولين أو الفرق.", "data-insight-alert-trigger-description": "التشغيل في الوقت الفعلي أو جدولته يوميًا أو أسبوعيًا أو شهريًا.", "data-insight-message": "إدارة سير عمل رؤى البيانات.", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "Execution history and test suite will be preserved", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "في قاعدة البيانات هذه", + "incident-metrics-sub-header": "تتبع حجم واستجابة وكفاءة حل الحوادث المتعلقة بالبيانات لتحسين الموثوقية التشغيلية.", "include-assets-message": "تفعيل استخراج {{assets}} من مصدر البيانات.", "include-database-filter-extra-information": "قاعدة البيانات التي تمت إضافتها أثناء إنشاء الخدمة.", "include-lineage-message": "تكوين لإيقاف جلب سلسلة البيانات من خطوط الأنابيب.", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "الوصول إلى عرض مركزي لصحة مجموعة البيانات الخاصة بك بناءً على التحققات الاختبارية المكونة.", "test-case-name-validation": "لا يمكن أن يحتوي الاسم على نقطتين مزدوجتين (::)، علامات اقتباس (\")، أو رموز أكبر من (>).", "test-case-schedule-description": "يمكن جدولة اختبارات جودة البيانات للتشغيل بالتردد المطلوب. المنطقة الزمنية هي التوقيت العالمي المنسق (UTC).", + "test-case-status-sub-header": "اعرض بصرياً صحة اختبارات التحقق من البيانات الخاصة بك مع تفصيل لحالات النجاح والفشل والإلغاء.", "test-connection-cannot-be-triggered": "لا يمكن تشغيل اختبار الاتصال.", "test-connection-taking-too-long": { "default": "افتراضي", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json index 293d321e9617..601d3e16d92a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json @@ -451,10 +451,13 @@ "data-contract-plural": "Datenverträge", "data-contract-status": "Datenvertragsstatus", "data-count-plural": "Datenanzahl", + "data-dimensions": "Datendimensionen", "data-discovery": "Datenentdeckung", "data-distribution": "Datenaufteilung", "data-entity": "Daten-Entität {{entity}}", + "data-health": "Datengesundheit", "data-health-by-entity": "Datenqualität nach {{entity}}", + "data-health-overview": "Data health overview", "data-insight": "Datenüberblick", "data-insight-active-user-summary": "Aktivste Benutzer", "data-insight-chart": "Datenüberblicksdiagramm", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "Inaktive Ankündigungen", "incident": "Vorfall", "incident-manager": "Vorfallmanager", + "incident-metrics": "Störfall-Metriken", "incident-plural": "Vorfälle", "incident-status": "Vorfallstatus", "include": "Einschließen", @@ -1999,6 +2003,7 @@ "test-case-plural": "Testfälle", "test-case-resolution-status": "Testfall-Auflösungsstatus", "test-case-result": "Testfall Ergebnisse", + "test-case-status": "Testfall-Status", "test-definition": "Testdefinition", "test-definition-plural": "Testdefinitionen", "test-email": "E-Mail testen", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "Sicherheits- und Zugangsrichtlinien-Erwartungen", "data-contract-sla-description": "Erwartungen an Service-Level-Vereinbarung", "data-contract-terms-of-service-description": "Die Regeln, denen Sie bei der Nutzung eines Dienstes zustimmen", + "data-dimensions-sub-header": "Gewinnen Sie Einblicke in wichtige Datenqualitätsdimensionen wie Genauigkeit, Konsistenz und Integrität, um die Vollständigkeit und Zuverlässigkeit Ihrer Daten zu bewerten.", + "data-health-sub-header": "Verfolgen Sie die Abdeckung, Qualität und Testergebnisse Ihrer Datenbestände, um die allgemeine Datengesundheit zu überwachen und Verbesserungsbereiche zu identifizieren.", "data-insight-alert-destination-description": "Senden Sie E-Mail-Benachrichtigungen an Administratoren oder Teams.", "data-insight-alert-trigger-description": "Auslöser für Echtzeit oder planen Sie ihn für täglich, wöchentlich oder monatlich.", "data-insight-message": "Verwalten Sie die Dateninsights-Pipeline.", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "Der Ausführungsverlauf und die Testsuite bleiben erhalten", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "In dieser Datenbank", + "incident-metrics-sub-header": "Verfolgen Sie das Volumen, die Reaktionsfähigkeit und die Lösungseffizienz von datenbezogenen Vorfällen, um die operative Zuverlässigkeit zu verbessern.", "include-assets-message": "Aktivieren Sie die Extraktion von {{assets}} aus der Datenquelle.", "include-database-filter-extra-information": "Die beim Erstellen des Dienstes hinzugefügte Datenbank.", "include-lineage-message": "Konfiguration zum Deaktivieren des Abrufs von Verbindungsdaten aus Pipelines.", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "Zugriff auf eine zentrale Ansicht der Gesundheit Ihres Datensatzes basierend auf konfigurierten Testvalidierungen.", "test-case-name-validation": "Der Name darf keine doppelten Doppelpunkte (::), Anführungszeichen (\") oder Größer-als-Symbole (>) enthalten.", "test-case-schedule-description": "Die Datenqualitätstests können mit der gewünschten Häufigkeit geplant werden. Die Zeitzone ist UTC.", + "test-case-status-sub-header": "Visualisieren Sie die Gesundheit Ihrer Datenvalidierungstests mit einer Aufschlüsselung von erfolgreichen, fehlgeschlagenen und abgebrochenen Fällen.", "test-connection-cannot-be-triggered": "Die Testverbindung kann nicht ausgelöst werden.", "test-connection-taking-too-long": { "default": "Bitte stellen Sie sicher, dass {{service_type}} so konfiguriert ist, dass Verbindungen erlaubt sind", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index 12118c7d6481..ba9daad263df 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -451,10 +451,13 @@ "data-contract-plural": "Data Contracts", "data-contract-status": "Data Contract Status", "data-count-plural": "Data Counts", + "data-dimensions": "Data Dimensions", "data-discovery": "Data Discovery", "data-distribution": "Data Distribution", "data-entity": "Data {{entity}}", + "data-health": "Data Health", "data-health-by-entity": "Data health by {{entity}}", + "data-health-overview": "Data health overview", "data-insight": "Data Insight", "data-insight-active-user-summary": "Most Active Users", "data-insight-chart": "Data Insight Chart", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "Inactive Announcements", "incident": "Incident", "incident-manager": "Incident Manager", + "incident-metrics": "Incident Metrics", "incident-plural": "Incidents", "incident-status": "Incident Status", "include": "Include", @@ -1999,6 +2003,7 @@ "test-case-plural": "Test Cases", "test-case-resolution-status": "Test Case Resolution Status", "test-case-result": "Test Case Results", + "test-case-status": "Test Case Status", "test-definition": "Test Definition", "test-definition-plural": "Test Definitions", "test-email": "Test Email", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "Security and access policy expectations", "data-contract-sla-description": "Service Level Agreement Expectations", "data-contract-terms-of-service-description": "The rules you agree to when using a service", + "data-dimensions-sub-header": "Gain insights into key data quality dimensions—such as accuracy, consistency, and integrity—to assess the completeness and reliability of your data.", + "data-health-sub-header": "Track the coverage, quality, and test results of your data assets to monitor overall data health and identify areas for improvement.", "data-insight-alert-destination-description": "Send email notifications to admins or teams.", "data-insight-alert-trigger-description": "Trigger for real time or schedule it for daily, weekly or monthly.", "data-insight-message": "Manage data insights pipeline.", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "Execution history and test suite will be preserved", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "In this Database", + "incident-metrics-sub-header": "Track the volume, responsiveness, and resolution efficiency of data-related incidents to improve operational reliability.", "include-assets-message": "Enable extracting {{assets}} from the data source.", "include-database-filter-extra-information": "Database which was added while creating service.", "include-lineage-message": "Configuration to turn off fetching lineage from pipelines.", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "Access a centralized view of your dataset's health based on configured test validations.", "test-case-name-validation": "Name cannot contain double colons (::), quotes (\"), or greater-than symbols (>).", "test-case-schedule-description": "The data quality tests can be scheduled to run at the desired frequency. The timezone is in UTC.", + "test-case-status-sub-header": "Visualize the health of your data validation tests with a breakdown of success, failure, and aborted cases.", "test-connection-cannot-be-triggered": "Test connection cannot be triggered.", "test-connection-taking-too-long": { "default": "Please ensure that {{service_type}} is configured to allow connections", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json index 617bc3eb2c35..d5f97fa98d0b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json @@ -451,10 +451,13 @@ "data-contract-plural": "Contratos de Datos", "data-contract-status": "Estado del Contrato de Datos", "data-count-plural": "Conteo de Datos", + "data-dimensions": "Dimensiones de Datos", "data-discovery": "Descubrimiento de Datos", "data-distribution": "Distribución de Datos", "data-entity": "{{entity}} de datos", + "data-health": "Salud de los Datos", "data-health-by-entity": "Salud de los datos por {{entity}}", + "data-health-overview": "Data health overview", "data-insight": "Análisis de datos", "data-insight-active-user-summary": "Usuarios más activos", "data-insight-chart": "Gráfico de Análisis de datos", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "Anuncios inactivos", "incident": "Incidente", "incident-manager": "Gestor de Incidentes", + "incident-metrics": "Métricas de Incidentes", "incident-plural": "Incidentes", "incident-status": "Estado del Incidente", "include": "Incluir", @@ -1999,6 +2003,7 @@ "test-case-plural": "Casos de Prueba", "test-case-resolution-status": "Estado de Resolución del Caso de Prueba", "test-case-result": "Resultados del caso de prueba", + "test-case-status": "Estado de Casos de Prueba", "test-definition": "Definición de prueba", "test-definition-plural": "Definiciones de prueba", "test-email": "Email de prueba", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "Expectativas de políticas de seguridad y acceso", "data-contract-sla-description": "Expectativas del Acuerdo de Nivel de Servicio", "data-contract-terms-of-service-description": "Las reglas con las que estás de acuerdo al usar un servicio", + "data-dimensions-sub-header": "Obtenga información sobre las dimensiones clave de calidad de datos—como precisión, consistencia e integridad—para evaluar la completitud y confiabilidad de sus datos.", + "data-health-sub-header": "Rastree la cobertura, calidad y resultados de pruebas de sus activos de datos para monitorear la salud general de los datos e identificar áreas de mejora.", "data-insight-alert-destination-description": "Enviar notificaciones por correo electrónico a administradores o equipos.", "data-insight-alert-trigger-description": "Desencadenador para tiempo real o prográmelo para diario, semanal o mensualmente.", "data-insight-message": "Administrar el pipeline de información de datos.", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "Se conservarán el historial de ejecución y el conjunto de pruebas.", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "En esta base de datos", + "incident-metrics-sub-header": "Rastree el volumen, capacidad de respuesta y eficiencia de resolución de incidentes relacionados con datos para mejorar la confiabilidad operacional.", "include-assets-message": "Configuración opcional para activar la ingestión de {{assets}}.", "include-database-filter-extra-information": "Base de datos que se agregó al crear el servicio.", "include-lineage-message": "Configuración para desactivar la obtención de linaje desde pipelines.", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "Accede a una vista centralizada de la salud de tu conjunto de datos basada en las validaciones de prueba configuradas.", "test-case-name-validation": "El nombre no puede contener dos puntos dobles (::), comillas (\") o símbolos de mayor que (>).", "test-case-schedule-description": "Los tests de calidad de datos pueden ser programados para ser ejecutados a la frecuencia deseada. El timezone es en UTC.", + "test-case-status-sub-header": "Visualice la salud de sus pruebas de validación de datos con un desglose de casos exitosos, fallidos y abortados.", "test-connection-cannot-be-triggered": "No se puede iniciar la prueba de conexión.", "test-connection-taking-too-long": { "default": "Por favor, asegúrese de que {{service_type}} esté configurado para permitir conexiones", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index bac57518067b..2532a38c1ffd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -451,10 +451,13 @@ "data-contract-plural": "Contrats de Données", "data-contract-status": "Statut du Contrat de Données", "data-count-plural": "Décompte des Données", + "data-dimensions": "Dimensions des Données", "data-discovery": "Découverte des Données", "data-distribution": "Distribution des Données", "data-entity": "Entité de Données {{entity}}", + "data-health": "Santé des Données", "data-health-by-entity": "Santé des données par {{entity}}", + "data-health-overview": "Data health overview", "data-insight": "Aperçu des Données", "data-insight-active-user-summary": "Utilisateurs les plus Actifs", "data-insight-chart": "Graphique de l'Aperçu des Données", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "Annonces Inactives", "incident": "Incident", "incident-manager": "Gestionnaire d'Incidents", + "incident-metrics": "Métriques d'Incident", "incident-plural": "Incidents", "incident-status": "Statut de l'Incident", "include": "Inclure", @@ -1999,6 +2003,7 @@ "test-case-plural": "Cas de Tests", "test-case-resolution-status": "Statut de Résolution du Cas de Test", "test-case-result": "Résultat du cas de test", + "test-case-status": "Statut des Cas de Test", "test-definition": "Définition du test", "test-definition-plural": "Définitions des tests", "test-email": "E-mail de test", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "Attentes des politiques de sécurité et d'accès", "data-contract-sla-description": "Attentes de l'Accord de Niveau de Service", "data-contract-terms-of-service-description": "Les règles que vous acceptez lorsque vous utilisez un service", + "data-dimensions-sub-header": "Obtenez des informations sur les dimensions clés de la qualité des données—telles que la précision, la cohérence et l'intégrité—pour évaluer la complétude et la fiabilité de vos données.", + "data-health-sub-header": "Suivez la couverture, la qualité et les résultats des tests de vos actifs de données pour surveiller la santé globale des données et identifier les domaines d'amélioration.", "data-insight-alert-destination-description": "Envoyez des notifications par email aux administrateurs ou aux équipes.", "data-insight-alert-trigger-description": "Déclenchez en temps réel ou programmez-le quotidiennement, hebdomadairement ou mensuellement.", "data-insight-message": "Gérer la pipeline d'analytiques.", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "L'historique d'exécution et la suite de tests seront préservés", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "Dans cette base de données", + "incident-metrics-sub-header": "Suivez le volume, la réactivité et l'efficacité de résolution des incidents liés aux données pour améliorer la fiabilité opérationnelle.", "include-assets-message": "Activer l'extraction des {{assets}} de la source de données.", "include-database-filter-extra-information": "Base de données qui a été ajoutée lors de la création du service.", "include-lineage-message": "Configuration pour désactiver la récupération de la lignée à partir des pipelines.", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "Accédez à une vue centralisée de la santé de votre dataset basée sur les validations de test configurées.", "test-case-name-validation": "Le nom ne peut pas contenir de double deux-points (::), de guillemets (\") ou de symboles supérieur à (>).", "test-case-schedule-description": "Les tests de quelité de données peuvent être planifiés pour tourner à la fréquence désirée. La timezone est en UTC.", + "test-case-status-sub-header": "Visualisez la santé de vos tests de validation de données avec une ventilation des cas de succès, d'échec et d'abandon.", "test-connection-cannot-be-triggered": "Le test de connexion ne peut pas être déclenché.", "test-connection-taking-too-long": { "default": "Veuillez vérifier que {{service_type}} est configuré pour autoriser les connexions", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json index ae17e51ad921..55a05cd47bc9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json @@ -451,10 +451,13 @@ "data-contract-plural": "Contratos de datos", "data-contract-status": "Estado do Contrato de Datos", "data-count-plural": "Reconto de datos", + "data-dimensions": "Dimensións de Datos", "data-discovery": "Descubrimento de datos", "data-distribution": "Distribución de datos", "data-entity": "Datos {{entity}}", + "data-health": "Saúde dos Datos", "data-health-by-entity": "Saúde dos datos por {{entity}}", + "data-health-overview": "Data health overview", "data-insight": "Perspectiva de datos", "data-insight-active-user-summary": "Usuarios máis activos", "data-insight-chart": "Gráfico de perspectiva de datos", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "Anuncios inactivos", "incident": "Incidente", "incident-manager": "Xestor de incidentes", + "incident-metrics": "Métricas de Incidentes", "incident-plural": "Incidentes", "incident-status": "Estado do incidente", "include": "Incluir", @@ -1999,6 +2003,7 @@ "test-case-plural": "Casos de proba", "test-case-resolution-status": "Estado da Resolución do Caso de Proba", "test-case-result": "Resultados do caso de proba", + "test-case-status": "Estado dos Casos de Proba", "test-definition": "Test Definition", "test-definition-plural": "Test Definitions", "test-email": "Correo electrónico de proba", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "Expectativas de seguridade e política de acceso", "data-contract-sla-description": "Expectativas do Acordo de Nivel de Servicio", "data-contract-terms-of-service-description": "As regras coas que acordas ao usar un servizo", + "data-dimensions-sub-header": "Obtén información sobre as dimensións clave da calidade dos datos—como a precisión, consistencia e integridade—para avaliar a completude e fiabilidade dos teus datos.", + "data-health-sub-header": "Fai un seguimento da cobertura, calidade e resultados de probas dos teus activos de datos para monitorizar a saúde xeral dos datos e identificar areas de mellora.", "data-insight-alert-destination-description": "Envía notificacións por correo electrónico aos administradores ou equipos.", "data-insight-alert-trigger-description": "Activador en tempo real ou programado para diario, semanal ou mensual.", "data-insight-message": "Xestiona o pipeline de insights de datos.", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "Execution history and test suite will be preserved", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "Nesta base de datos", + "incident-metrics-sub-header": "Fai un seguimento do volume, capacidade de resposta e eficiencia de resolución de incidentes relacionados cos datos para mellorar a fiabilidade operacional.", "include-assets-message": "Activar a extracción de {{assets}} desde a fonte de datos.", "include-database-filter-extra-information": "Base de datos engadida ao crear o servizo.", "include-lineage-message": "Configuración para desactivar a obtención de liñaxe de pipelines.", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "Accede a unha vista centralizada da saúde do teu conxunto de datos baseada en validacións de proba configuradas.", "test-case-name-validation": "O nome non pode conter dous puntos duplos (::), comiñas (\") ou símbolos de maior que (>).", "test-case-schedule-description": "As probas de calidade de datos poden programarse para executarse coa frecuencia desexada. A zona horaria está en UTC.", + "test-case-status-sub-header": "Visualiza a saúde das túas probas de validación de datos cunha desagregación de casos de éxito, fallo e abortados.", "test-connection-cannot-be-triggered": "Non se pode iniciar a conexión de proba.", "test-connection-taking-too-long": { "default": "Asegúrese de que {{service_type}} estea configurado para permitir conexións", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json index 1cbd6a188276..15e870fce547 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json @@ -451,10 +451,13 @@ "data-contract-plural": "חוזי נתונים", "data-contract-status": "סטטוס חוזה נתונים", "data-count-plural": "ספירת נתונים", + "data-dimensions": "ממדי נתונים", "data-discovery": "חקירת נתונים (Discovery)", "data-distribution": "הפצת נתונים", "data-entity": "נתון {{entity}}", + "data-health": "בריאות הנתונים", "data-health-by-entity": "בריאות הנתונים לפי {{entity}}", + "data-health-overview": "Data health overview", "data-insight": "תובנת נתונים", "data-insight-active-user-summary": "משתמשים הפעילים ביותר", "data-insight-chart": "תרשים תובנת נתונים", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "הכרזות לא פעילות", "incident": "תקרית", "incident-manager": "מנהל אירועים", + "incident-metrics": "מדדים של אירועים", "incident-plural": "תקריות", "incident-status": "סטטוס תקרית", "include": "כלול", @@ -1999,6 +2003,7 @@ "test-case-plural": "בדיקות נתונים", "test-case-resolution-status": "סטטוס פתרון מקרי בדיקה", "test-case-result": "תוצאות מקרה בדיקה", + "test-case-status": "מצב מקרה בדיקה", "test-definition": "הגדרת בדיקה", "test-definition-plural": "הגדרות בדיקה", "test-email": "בדוק דוא\"ל", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "ציפיות למדיניות אבטחה וגישה", "data-contract-sla-description": "ציפיות להסכם רמת שירות", "data-contract-terms-of-service-description": "החוקים שאתה מסכים להם כאשר אתה משתמש בשירות", + "data-dimensions-sub-header": "קבל תובנות על מימדים מרכזיים של איכות הנתונים—כמו דיוק, עקביות ושלמות—כדי להעריך את השלמות והאמינות של הנתונים שלך.", + "data-health-sub-header": "עקב אחר הכיסוי, האיכות ותוצאות הבדיקות של נכסי הנתונים שלך כדי לעקוב אחר בריאות הנתונים הכללית ולזהות אזורים לשיפור.", "data-insight-alert-destination-description": "שלח התראות בדואר אלקטרוני למנהלים או לצוותים.", "data-insight-alert-trigger-description": "הפעל לזמן אמת או קבע את זה לתדירויות יומיומיות, שבועיות או חודשיות.", "data-insight-message": "נהל את תהליך טעינת תצוגת נתונים.", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "היסטוריית ביצוע וחבילת בדיקות יישמרו", "import-odcs-replace-preserve-id": "מזהה החוזה, FQN והפניות לישות יישמרו", "in-this-database": "במסד נתונים זה", + "incident-metrics-sub-header": "עקב אחר ההיקף, היעילות ויעילות הפתרון של אירועים הקשורים לנתונים כדי לשפר את האמינות התפעולית.", "include-assets-message": "הפעל את החילוץ של {{assets}} ממקור הנתונים.", "include-database-filter-extra-information": "מסד הנתונים שנוסף בעת יצירת השירות.", "include-lineage-message": "תצורה לכיבוי של גריסת lineage מתוך תהליכי טעינת נתונים.", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "גישה לתצוגה מרכזית של בריאות מערך הנתונים שלך על בסיס אימותי בדיקה מוגדרים.", "test-case-name-validation": "השם לא יכול להכיל נקודתיים כפולות (::), מרכאות (\") או סמלי גדול מ- (>).", "test-case-schedule-description": "בדיקות איכות הנתונים ניתנות לתזמון לרוץ בתדירות הרצויה. אזור הזמן הוא UTC.", + "test-case-status-sub-header": "הצג את בריאות בדיקות האימות של הנתונים שלך עם פילוח של מקרים מוצלחים, כושלים ומופסקים.", "test-connection-cannot-be-triggered": "אין אפשרות להפעיל בדיקת חיבור.", "test-connection-taking-too-long": { "default": "אנא וודא ש‑{{service_type}} מוגדר לאפשר חיבורים", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json index 088a0079b1bd..8224b8f46f76 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json @@ -451,10 +451,13 @@ "data-contract-plural": "データ契約", "data-contract-status": "データ契約ステータス", "data-count-plural": "データ件数", + "data-dimensions": "データディメンション", "data-discovery": "データ探索", "data-distribution": "データ分布", "data-entity": "データ{{entity}}", + "data-health": "データヘルス", "data-health-by-entity": "{{entity}}ごとのデータ健全性", + "data-health-overview": "Data health overview", "data-insight": "データインサイト", "data-insight-active-user-summary": "最も活動的なユーザー", "data-insight-chart": "データインサイトチャート", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "非アクティブなお知らせ", "incident": "インシデント", "incident-manager": "インシデントマネージャー", + "incident-metrics": "インシデントメトリクス", "incident-plural": "インシデント", "incident-status": "インシデントステータス", "include": "含める", @@ -1999,6 +2003,7 @@ "test-case-plural": "テストケース", "test-case-resolution-status": "テストケースの解決状況", "test-case-result": "テストケース結果", + "test-case-status": "テストケースステータス", "test-definition": "テストの定義", "test-definition-plural": "テストの定義", "test-email": "テストメール", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "セキュリティおよびアクセス ポリシーへの期待", "data-contract-sla-description": "サービス レベル契約に関する期待", "data-contract-terms-of-service-description": "サービス利用時に同意するルール", + "data-dimensions-sub-header": "正確性、一貫性、整合性などの主要なデータ品質の次元に関する洞察を得て、データの完全性と信頼性を評価します。", + "data-health-sub-header": "データアセットのカバレッジ、品質、テスト結果を追跡して全体的なデータヘルスをモニタリングし、改善領域を特定します。", "data-insight-alert-destination-description": "通知は管理者またはチームにメールで送信されます。", "data-insight-alert-trigger-description": "リアルタイムまたは日次、週次、月次でのトリガーを設定可能です。", "data-insight-message": "データインサイトパイプラインを管理します。", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "実行履歴とテストスイートは保存されます", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "このデータベース内", + "incident-metrics-sub-header": "データ関連インシデントのボリューム、応答性、解決効率を追跡して運用の信頼性を向上させます。", "include-assets-message": "{{assets}} をデータソースから抽出可能にする", "include-database-filter-extra-information": "サービス作成時に追加されたデータベースです", "include-lineage-message": "パイプラインからリネージを取得するかを設定します", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "設定されたテストバリデーションに基づいて、データセットの健全性を集中して確認できます。", "test-case-name-validation": "名前にはダブルコロン(::)、引用符(\")、またはより大きい記号(>)を含めることはできません。", "test-case-schedule-description": "データ品質テストは希望の頻度でスケジュール実行できます。タイムゾーンはUTCです。", + "test-case-status-sub-header": "成功、失敗、中断されたケースの内訳とともに、データ検証テストのヘルスを視覚化します。", "test-connection-cannot-be-triggered": "接続テストを開始できません。", "test-connection-taking-too-long": { "default": "{{service_type}} が接続を許可するように構成されていることを確認してください", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json index d9ea3f348559..43c47b220620 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json @@ -451,10 +451,13 @@ "data-contract-plural": "데이터 계약", "data-contract-status": "데이터 계약 상태", "data-count-plural": "데이터 개수", + "data-dimensions": "데이터 차원", "data-discovery": "데이터 발견", "data-distribution": "데이터 분포", "data-entity": "데이터 {{entity}}", + "data-health": "데이터 상태", "data-health-by-entity": "{{entity}}별 데이터 건강 상태", + "data-health-overview": "Data health overview", "data-insight": "데이터 인사이트", "data-insight-active-user-summary": "가장 활성화된 사용자", "data-insight-chart": "데이터 인사이트 차트", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "비활성 공지사항들", "incident": "사고", "incident-manager": "사고 관리자", + "incident-metrics": "인시던트 지표", "incident-plural": "사고", "incident-status": "사고 상태", "include": "포함", @@ -1999,6 +2003,7 @@ "test-case-plural": "테스트 케이스들", "test-case-resolution-status": "테스트 케이스 해결 상태", "test-case-result": "테스트 케이스 결과", + "test-case-status": "테스트 케이스 상태", "test-definition": "Test Definition", "test-definition-plural": "Test Definitions", "test-email": "이메일 테스트", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "보안 및 접근 정책 기대사항", "data-contract-sla-description": "서비스 수준 협약(SLA)에 대한 기대", "data-contract-terms-of-service-description": "서비스를 사용할 때 동의하는 규칙", + "data-dimensions-sub-header": "정확성, 일관성, 무결성과 같은 주요 데이터 품질 차원에 대한 통찰을 얻어 데이터의 완전성과 신뢰성을 평가합니다.", + "data-health-sub-header": "전반적인 데이터 상태를 모니터링하고 개선이 필요한 영역을 식별하기 위해 데이터 자산의 커버리지, 품질 및 테스트 결과를 추적합니다.", "data-insight-alert-destination-description": "관리자 또는 팀에게 이메일 알림을 보냅니다.", "data-insight-alert-trigger-description": "실시간으로 트리거하거나 일별, 주별 또는 월별로 예약합니다.", "data-insight-message": "데이터 인사이트 파이프라인을 관리합니다.", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "Execution history and test suite will be preserved", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "이 데이터베이스에서", + "incident-metrics-sub-header": "운영 신뢰성을 향상시키기 위해 데이터 관련 인시던트의 볼륨, 응답성 및 해결 효율성을 추적합니다.", "include-assets-message": "데이터 소스에서 {{assets}} 추출을 활성화합니다.", "include-database-filter-extra-information": "서비스 생성 시 추가된 데이터베이스입니다.", "include-lineage-message": "파이프라인에서 계보 가져오기를 끄는 구성입니다.", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "구성된 테스트 검증을 기반으로 데이터셋 상태의 중앙 집중식 보기에 액세스합니다.", "test-case-name-validation": "이름에는 이중 콜론(::), 따옴표(\"), 또는 보다 큰 기호(>)를 포함할 수 없습니다.", "test-case-schedule-description": "데이터 품질 테스트는 원하는 빈도로 실행되도록 예약할 수 있습니다. 시간대는 UTC입니다.", + "test-case-status-sub-header": "성공, 실패 및 중단된 케이스의 분류와 함께 데이터 유효성 검사 테스트의 상태를 시각화합니다.", "test-connection-cannot-be-triggered": "연결 테스트를 트리거할 수 없습니다.", "test-connection-taking-too-long": { "default": "{{service_type}} 가 연결을 허용하도록 구성되어 있는지 확인하세요", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json index b4d92ec19432..d0005a33f862 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json @@ -451,10 +451,13 @@ "data-contract-plural": "डेटा करार", "data-contract-status": "Data Contract Status", "data-count-plural": "डेटा संख्या", + "data-dimensions": "डेटा परिमाण", "data-discovery": "डेटा शोध", "data-distribution": "डेटा वितरण", "data-entity": "डेटा {{entity}}", + "data-health": "डेटा आरोग्य", "data-health-by-entity": "{{entity}} नुसार डेटाची स्थिती", + "data-health-overview": "Data health overview", "data-insight": "डेटा अंतर्दृष्टी", "data-insight-active-user-summary": "सर्वात सक्रिय वापरकर्ते", "data-insight-chart": "डेटा अंतर्दृष्टी तक्ता", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "निष्क्रिय घोषणा", "incident": "घटना", "incident-manager": "घटना व्यवस्थापक", + "incident-metrics": "घटना मेट्रिक्स", "incident-plural": "घटना", "incident-status": "घटना स्थिती", "include": "समाविष्ट करा", @@ -1999,6 +2003,7 @@ "test-case-plural": "चाचणी प्रकरणे", "test-case-resolution-status": "टेस्ट केस स्थिती सुलभीकरण", "test-case-result": "चाचणी प्रकरण परिणाम", + "test-case-status": "चाचणी प्रकरण स्थिती", "test-definition": "Test Definition", "test-definition-plural": "Test Definitions", "test-email": "चाचणी ईमेल", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "डेटा सुरक्षा आणि प्रवेश धोरण संबंधित अपेक्षा", "data-contract-sla-description": "सेवा पातळी कराराची अपेक्षा", "data-contract-terms-of-service-description": "सेवा वापरताना आपण ज्या नियमांवर सहमत असता", + "data-dimensions-sub-header": "अचूकता, सुसंगतता आणि अखंडता यासारख्या प्रमुख डेटा गुणवत्ता आयामांबद्दल अंतर्दृष्टी मिळवा आणि आपल्या डेटाची पूर्णता आणि विश्वासार्हता मूल्यांकन करा.", + "data-health-sub-header": "एकूण डेटा आरोग्याचे निरीक्षण करण्यासाठी आणि सुधारणेची क्षेत्रे ओळखण्यासाठी आपल्या डेटा मालमत्तांचे कव्हरेज, गुणवत्ता आणि चाचणी परिणाम ट्रॅक करा.", "data-insight-alert-destination-description": "प्रशासक किंवा टीमला ईमेल सूचना पाठवा.", "data-insight-alert-trigger-description": "रिअल टाइमसाठी ट्रिगर करा किंवा ते दैनिक, साप्ताहिक किंवा मासिक शेड्यूल करा.", "data-insight-message": "डेटा अंतर्दृष्टी पाइपलाइन व्यवस्थापित करा.", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "Execution history and test suite will be preserved", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "या डेटाबेसमध्ये", + "incident-metrics-sub-header": "ऑपरेशनल विश्वासार्हता सुधारण्यासाठी डेटा-संबंधित घटनांचे प्रमाण, प्रतिसादशीलता आणि निराकरण कार्यक्षमता ट्रॅक करा.", "include-assets-message": "डेटा स्रोतातून {{assets}} काढणे सक्षम करा.", "include-database-filter-extra-information": "सेवा तयार करताना जोडलेला डेटाबेस.", "include-lineage-message": "पाइपलाइनमधून वंशावळ मिळवणे बंद करण्यासाठी संरचना.", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "कॉन्फिगर केलेल्या चाचणी प्रमाणीकरणांवर आधारित आपल्या डेटासेटच्या आरोग्याचे केंद्रीकृत दृश्य प्रवेश करा.", "test-case-name-validation": "नावामध्ये डबल कॉलन (::), अवतरण (\") किंवा मोठे चिन्ह (>) असू शकत नाही.", "test-case-schedule-description": "डेटा गुणवत्ता चाचण्या इच्छित वारंवारतेवर चालवण्यासाठी वेळापत्रक करता येतात. टाइमझोन UTC मध्ये आहे.", + "test-case-status-sub-header": "यशस्वी, अयशस्वी आणि रद्द झालेल्या प्रकरणांच्या विभाजनासह आपल्या डेटा प्रमाणीकरण चाचण्यांचे आरोग्य व्हिज्युअलाइझ करा.", "test-connection-cannot-be-triggered": "कनेक्शन चाचणी ट्रिगर केली जाऊ शकत नाही.", "test-connection-taking-too-long": { "default": "कृपया खात्री करा की {{service_type}} कनेक्शनसाठी कॉन्फिगर केले गेले आहे", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json index 30c52046855c..146071737781 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json @@ -451,10 +451,13 @@ "data-contract-plural": "Datacontracten", "data-contract-status": "Data contract status", "data-count-plural": "Hoeveelheid data", + "data-dimensions": "Datadimensies", "data-discovery": "Data Discovery", "data-distribution": "Datadistributie", "data-entity": "Data {{entity}}", + "data-health": "Datakwaliteit", "data-health-by-entity": "Datakwaliteit per {{entity}}", + "data-health-overview": "Data health overview", "data-insight": "Data-inzicht", "data-insight-active-user-summary": "Meest actieve gebruikers", "data-insight-chart": "Chart data-inzicht", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "Inactieve aankondigingen", "incident": "Incident", "incident-manager": "Incidentmanager", + "incident-metrics": "Incident Metrieken", "incident-plural": "Incidenten", "incident-status": "Incidentstatus", "include": "Inclusief", @@ -1999,6 +2003,7 @@ "test-case-plural": "Testcases", "test-case-resolution-status": "Status van oplossing van testcase", "test-case-result": "Testcaseresultaten", + "test-case-status": "Testcase Status", "test-definition": "Testdefinitie", "test-definition-plural": "Testdefinities", "test-email": "Test E-mail", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "Verwachtingen m.b.t. beveiligings- en toegangsbeleid", "data-contract-sla-description": "Verwachtingen m.b.t. Service Level Agreement", "data-contract-terms-of-service-description": "De regels waartoe je instemt bij het gebruik van een service", + "data-dimensions-sub-header": "Krijg inzicht in belangrijke datakwaliteitsdimensies—zoals nauwkeurigheid, consistentie en integriteit—om de volledigheid en betrouwbaarheid van uw gegevens te beoordelen.", + "data-health-sub-header": "Volg de dekking, kwaliteit en testresultaten van uw data-assets om de algehele datakwaliteit te monitoren en verbetergebieden te identificeren.", "data-insight-alert-destination-description": "Stuur e-mailmeldingen naar beheerders of teams.", "data-insight-alert-trigger-description": "Stel de trigger in op realtime of plan het voor dagelijks, wekelijks of maandelijks.", "data-insight-message": "Beheer data-inzichtpipeline.", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "De uitvoeringsgeschiedenis en het testpakket blijven behouden", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "In deze database", + "incident-metrics-sub-header": "Volg het volume, responsiviteit en resolutie-efficiëntie van data-gerelateerde incidenten om operationele betrouwbaarheid te verbeteren.", "include-assets-message": "Schakel het extraheren van {{assets}} uit de databron in.", "include-database-filter-extra-information": "Database die is toegevoegd tijdens het maken van de service.", "include-lineage-message": "Configuratie om het ophalen van lineage uit pipelines uit te schakelen.", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "Toegang tot een gecentraliseerde weergave van de gezondheid van uw dataset op basis van geconfigureerde testvalidaties.", "test-case-name-validation": "Naam mag geen dubbele dubbelpunten (::), aanhalingstekens (\") of groter-dan symbolen (>) bevatten.", "test-case-schedule-description": "De datakwaliteitstests kunnen worden gepland om op de gewenste frequentie te draaien. De tijdzone is UTC.", + "test-case-status-sub-header": "Visualiseer de gezondheid van uw datavalidatietests met een uitsplitsing van succesvolle, mislukte en afgebroken cases.", "test-connection-cannot-be-triggered": "Testconnectie kan niet worden geactiveerd.", "test-connection-taking-too-long": { "default": "Zorg ervoor dat {{service_type}} is geconfigureerd om verbindingen toe te staan", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json index e698b32bdfe5..b867474c7d48 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json @@ -451,10 +451,13 @@ "data-contract-plural": "قراردادهای داده", "data-contract-status": "Data Contract Status", "data-count-plural": "شمارش داده‌ها", + "data-dimensions": "ابعاد داده", "data-discovery": "کشف داده", "data-distribution": "توزیع داده", "data-entity": "{{entity}} داده", + "data-health": "سلامت داده", "data-health-by-entity": "سلامت داده‌ها بر اساس {{entity}}", + "data-health-overview": "Data health overview", "data-insight": "بینش داده", "data-insight-active-user-summary": "فعال‌ترین کاربران", "data-insight-chart": "نمودار بینش داده", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "اطلاعیه‌های غیرفعال", "incident": "حادثه", "incident-manager": "مدیر حادثه", + "incident-metrics": "معیارهای حوادث", "incident-plural": "Incidents", "incident-status": "وضعیت حادثه", "include": "شامل", @@ -1999,6 +2003,7 @@ "test-case-plural": "موردهای تست", "test-case-resolution-status": "وضعیت حل موارد آزمون", "test-case-result": "نتایج مورد تست", + "test-case-status": "وضعیت موارد آزمون", "test-definition": "Test Definition", "test-definition-plural": "Test Definitions", "test-email": "ایمیل تست", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "انتظارات سیاست امنیت و دسترسی", "data-contract-sla-description": "انتظارات توافق‌نامه سطح خدمات", "data-contract-terms-of-service-description": "قوانینی که هنگام استفاده از یک سرویس با آن موافقت می‌کنید", + "data-dimensions-sub-header": "بینش‌هایی درباره ابعاد کلیدی کیفیت داده—مانند دقت، سازگاری و یکپارچگی—برای ارزیابی کامل بودن و قابلیت اطمینان داده‌های خود به دست آورید.", + "data-health-sub-header": "پوشش، کیفیت و نتایج آزمایش دارایی‌های داده‌ای خود را برای نظارت بر سلامت کلی داده و شناسایی حوزه‌های بهبود پیگیری کنید.", "data-insight-alert-destination-description": "اعلان‌های ایمیلی را به مدیران یا تیم‌ها ارسال کنید.", "data-insight-alert-trigger-description": "برای زمان واقعی یا برنامه‌ریزی آن برای روزانه، هفتگی یا ماهانه را راه‌اندازی کنید.", "data-insight-message": "مدیریت پایپ‌لاین بینش داده.", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "Execution history and test suite will be preserved", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "در این پایگاه داده", + "incident-metrics-sub-header": "حجم، پاسخ و کارایی حل حوادث مرتبط با کیفیت داده را برای سنجش انعطاف‌پذیری داده پیگیری کنید.", "include-assets-message": "استخراج {{assets}} از منبع داده را فعال کنید.", "include-database-filter-extra-information": "پایگاه داده‌ای که هنگام ایجاد سرویس اضافه شده است.", "include-lineage-message": "پیکربندی برای خاموش کردن استخراج lineage از لوله‌های داده.", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "دسترسی به نمای متمرکز سلامت مجموعه داده‌های شما بر اساس اعتبارسنجی‌های آزمایش پیکربندی شده.", "test-case-name-validation": "نام نمی‌تواند شامل دو نقطه دوگانه (::)، نقل قول (\") یا نمادهای بزرگتر از (>) باشد.", "test-case-schedule-description": "آزمون‌های کیفیت داده می‌توانند به صورت دوره‌ای برنامه‌ریزی شوند. منطقه زمانی به صورت UTC است.", + "test-case-status-sub-header": "سلامت آزمون‌های تأیید داده‌ای خود را از طریق نمودارهای جامع و فهرست‌های وضعیت موارد آزمون به صورت بصری مشاهده کنید.", "test-connection-cannot-be-triggered": "اتصال تست نمی‌تواند اجرا شود.", "test-connection-taking-too-long": { "default": "لطفاً اطمینان حاصل کنید که {{service_type}} به درستی برای اجازه اتصال پیکربندی شده است", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json index aef3696b2a0d..b5782b254d42 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json @@ -451,10 +451,13 @@ "data-contract-plural": "Contratos de dados", "data-contract-status": "Status do Contrato de Dados", "data-count-plural": "Contagens de Dados", + "data-dimensions": "Data Dimensions", "data-discovery": "Descoberta de Dados", "data-distribution": "Distribuição de Dados", "data-entity": "Dados {{entity}}", + "data-health": "Saúde dos Dados", "data-health-by-entity": "Qualidade dos dados por {{entity}}", + "data-health-overview": "Data health overview", "data-insight": "Insight de Dados", "data-insight-active-user-summary": "Usuários Mais Ativos", "data-insight-chart": "Gráfico de Insight de Dados", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "Anúncios Inativos", "incident": "Incidente", "incident-manager": "Gestão de Incidente", + "incident-metrics": "Métricas de Incidentes", "incident-plural": "Incidentes", "incident-status": "Status do Incidente", "include": "Incluir", @@ -1999,6 +2003,7 @@ "test-case-plural": "Casos de Teste", "test-case-resolution-status": "Status de Resolução do Caso de Teste", "test-case-result": "Resultados do Caso de Teste", + "test-case-status": "Status dos Casos de Teste", "test-definition": "Definição de teste", "test-definition-plural": "Definições de teste", "test-email": "E-mail de Teste", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "Expectativas de políticas de segurança e acesso", "data-contract-sla-description": "Expectativas do Acordo de Nível de Serviço", "data-contract-terms-of-service-description": "As regras que você concorda ao usar um serviço", + "data-dimensions-sub-header": "Obtenha insights sobre as principais dimensões de qualidade de dados—como precisão, consistência e integridade—para avaliar a completude e confiabilidade dos seus dados.", + "data-health-sub-header": "Acompanhe a cobertura, qualidade e resultados de teste dos seus ativos de dados para monitorar a saúde geral dos dados e identificar áreas de melhoria.", "data-insight-alert-destination-description": "Envie notificações por e-mail para administradores ou equipes.", "data-insight-alert-trigger-description": "Gatilho para tempo real ou programe para diário, semanal ou mensal.", "data-insight-message": "Gerencie o pipeline de insights de dados.", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "O histórico de execução e o conjunto de testes serão preservados", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "Neste Banco de Dados", + "incident-metrics-sub-header": "Acompanhe o volume, responsividade e eficiência de resolução de incidentes relacionados a dados para melhorar a confiabilidade operacional.", "include-assets-message": "Habilite a extração de {{assets}} da fonte de dados.", "include-database-filter-extra-information": "Banco de dados que foi adicionado ao criar o serviço.", "include-lineage-message": "Configuração para desativar a busca de linhagem de pipelines.", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "Acesse uma visão centralizada da saúde do seu dataset baseada em validações de teste configuradas.", "test-case-name-validation": "O nome não pode conter dois pontos duplos (::), aspas (\") ou símbolos de maior que (>).", "test-case-schedule-description": "Os testes de qualidade de dados podem ser programados para serem executados na frequência desejada. O fuso horário está em UTC.", + "test-case-status-sub-header": "Visualize a saúde dos seus testes de validação de dados com uma divisão de casos de sucesso, falha e abortados.", "test-connection-cannot-be-triggered": "Não é possível acionar o teste de conexão.", "test-connection-taking-too-long": { "default": "Por favor, certifique‑se de que o {{service_type}} está configurado para permitir conexões", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json index de339a7d6230..a81745f26f27 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json @@ -451,10 +451,13 @@ "data-contract-plural": "Contratos de Dados", "data-contract-status": "Status do Contrato de Dados", "data-count-plural": "Contagens de Dados", + "data-dimensions": "Data Dimensions", "data-discovery": "Descoberta de Dados", "data-distribution": "Distribuição de Dados", "data-entity": "Dados {{entity}}", + "data-health": "Saúde dos Dados", "data-health-by-entity": "Qualidade dos dados por {{entity}}", + "data-health-overview": "Data health overview", "data-insight": "Insight de Dados", "data-insight-active-user-summary": "Utilizadores Mais Ativos", "data-insight-chart": "Gráfico de Insight de Dados", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "Anúncios Inativos", "incident": "Incidente", "incident-manager": "Gestão de Incidente", + "incident-metrics": "Métricas de Incidentes", "incident-plural": "Incidentes", "incident-status": "Status do Incidente", "include": "Incluir", @@ -1999,6 +2003,7 @@ "test-case-plural": "Casos de Teste", "test-case-resolution-status": "Estado da Resolução do Caso de Teste", "test-case-result": "Resultados do Caso de Teste", + "test-case-status": "Estado do Caso de Teste", "test-definition": "Test Definition", "test-definition-plural": "Test Definitions", "test-email": "E-mail de Teste", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "Expectativas de segurança e política de acesso", "data-contract-sla-description": "Expectativas de Acordo de Nível de Serviço", "data-contract-terms-of-service-description": "As regras com as quais concorda ao utilizar um serviço", + "data-dimensions-sub-header": "Obtenha insights sobre as principais dimensões da qualidade dos dados—como precisão, consistência e integridade—para avaliar a completude e confiabilidade dos seus dados.", + "data-health-sub-header": "Acompanhe a cobertura, qualidade e resultados dos testes dos seus ativos de dados para monitorizar a saúde geral dos dados e identificar áreas de melhoria.", "data-insight-alert-destination-description": "Envie notificações por e-mail para administradores ou equipas.", "data-insight-alert-trigger-description": "Gatilho para tempo real ou programe para diário, semanal ou mensal.", "data-insight-message": "Gerencie o pipeline de insights de dados.", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "O histórico de execução e o conjunto de testes serão preservados", "import-odcs-replace-preserve-id": "O ID do contrato, FQN e referência da entidade serão preservados", "in-this-database": "Neste Banco de Dados", + "incident-metrics-sub-header": "Acompanhe o volume, capacidade de resposta e eficiência de resolução de incidentes relacionados com dados para melhorar a confiabilidade operacional.", "include-assets-message": "Habilite a extração de {{assets}} da fonte de dados.", "include-database-filter-extra-information": "Banco de dados que foi adicionado ao criar o serviço.", "include-lineage-message": "Configuração para desativar a busca de linhagem de pipelines.", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "Aceda a uma vista centralizada da saúde do seu conjunto de dados baseada em validações de teste configuradas.", "test-case-name-validation": "O nome não pode conter dois pontos duplos (::), aspas (\") ou símbolos de maior que (>).", "test-case-schedule-description": "Os testes de qualidade de dados podem ser agendados para serem executados com a frequência desejada. O fuso horário está em UTC.", + "test-case-status-sub-header": "Visualize a saúde dos seus testes de validação de dados com uma desagregação de casos de sucesso, falha e abortados.", "test-connection-cannot-be-triggered": "Não é possível acionar o teste de conexão.", "test-connection-taking-too-long": { "default": "Por favor, assegure-se de que o {{service_type}} está configurado para permitir ligações", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json index 9f0cfa5405fa..39412b0c8f0d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json @@ -451,10 +451,13 @@ "data-contract-plural": "Дата-контракты", "data-contract-status": "Статус контракта данных", "data-count-plural": "Количество данных", + "data-dimensions": "Измерения данных", "data-discovery": "Обнаружение данных", "data-distribution": "Распределение данных", "data-entity": "{{entity}} данных", + "data-health": "Здоровье данных", "data-health-by-entity": "Качество данных по объекту «{{entity}}»", + "data-health-overview": "Data health overview", "data-insight": "Анализ данных", "data-insight-active-user-summary": "Самый активный пользователь", "data-insight-chart": "Диаграмма анализа данных", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "Неактивные объявления", "incident": "Инцидент", "incident-manager": "Управление инцидентами", + "incident-metrics": "Метрики инцидентов", "incident-plural": "Инциденты", "incident-status": "Статус инцидента", "include": "Включать", @@ -1999,6 +2003,7 @@ "test-case-plural": "Проверки", "test-case-resolution-status": "Статус решения тестового случая", "test-case-result": "Результат проверки", + "test-case-status": "Статус тестовых случаев", "test-definition": "Определение теста", "test-definition-plural": "Определения тестов", "test-email": "Протестировать электронную почту", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "Ожидания в отношении политики безопасности и доступа", "data-contract-sla-description": "Ожидания в отношении соглашения об уровне обслуживания", "data-contract-terms-of-service-description": "Правила, с которыми вы соглашаетесь при использовании услуги", + "data-dimensions-sub-header": "Получите понимание ключевых параметров качества данных—таких как точность, согласованность и целостность—для оценки полноты и надежности ваших данных.", + "data-health-sub-header": "Отслеживайте покрытие, качество и результаты тестов ваших активов данных для мониторинга общего здоровья данных и выявления областей для улучшения.", "data-insight-alert-destination-description": "Отправляйте уведомления по электронной почте администраторам или командам.", "data-insight-alert-trigger-description": "Запускайте в режиме реального времени или запланируйте его на день, неделю или месяц.", "data-insight-message": "Управление конвейером анализа данных.", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "История выполнения и набор тестов будут сохранены.", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "В этой базе данных", + "incident-metrics-sub-header": "Отслеживайте объем, скорость реагирования и эффективность разрешения инцидентов, связанных с данными, для улучшения операционной надежности.", "include-assets-message": "Включите извлечение {{assets}} из источника данных.", "include-database-filter-extra-information": "База данных, которая была добавлена при создании сервиса.", "include-lineage-message": "Конфигурация для отключения получения lineage из пайплайнов.", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "Доступ к централизованному представлению состояния вашего набора данных на основе настроенных проверок тестов.", "test-case-name-validation": "Имя не может содержать двойные двоеточия (::), кавычки (\") или символы больше (>).", "test-case-schedule-description": "Проверки качества данных можно поставить на расписание. Часовой пояс указан в UTC.", + "test-case-status-sub-header": "Визуализируйте здоровье ваших тестов валидации данных с разбивкой на успешные, неудачные и прерванные случаи.", "test-connection-cannot-be-triggered": "Тестовое соединение не может быть запущено.", "test-connection-taking-too-long": { "default": "Пожалуйста, убедитесь, что {{service_type}} настроен на разрешение подключений", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json index 3ef2c5459939..c57b6ab3faca 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json @@ -451,10 +451,13 @@ "data-contract-plural": "สัญญาข้อมูล", "data-contract-status": "Data Contract Status", "data-count-plural": "จำนวนข้อมูล", + "data-dimensions": "มิติข้อมูล", "data-discovery": "การค้นพบข้อมูล", "data-distribution": "การแจกจ่ายข้อมูล", "data-entity": "ข้อมูล {{entity}}", + "data-health": "สุขภาพข้อมูล", "data-health-by-entity": "คุณภาพข้อมูลตาม {{entity}}", + "data-health-overview": "Data health overview", "data-insight": "ข้อมูลเชิงลึก", "data-insight-active-user-summary": "ผู้ใช้ที่ใช้งานมากที่สุด", "data-insight-chart": "แผนภูมิข้อมูลเชิงลึก", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "ประกาศที่ไม่ใช้งาน", "incident": "เหตุการณ์", "incident-manager": "ผู้จัดการเหตุการณ์", + "incident-metrics": "เมตริกอุบัติการณ์", "incident-plural": "เหตุการณ์หลายรายการ", "incident-status": "สถานะเหตุการณ์", "include": "รวม", @@ -1999,6 +2003,7 @@ "test-case-plural": "กรณีทดสอบหลายรายการ", "test-case-resolution-status": "สถานะการแก้ไขกรณีทดสอบ", "test-case-result": "ผลกรณีทดสอบ", + "test-case-status": "สถานะเคสทดสอบ", "test-definition": "Test Definition", "test-definition-plural": "Test Definitions", "test-email": "อีเมลทดสอบ", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "ความคาดหวังด้านความปลอดภัยและนโยบายการเข้าถึง", "data-contract-sla-description": "ความคาดหวังเกี่ยวกับข้อตกลงระดับการให้บริการ", "data-contract-terms-of-service-description": "ข้อกำหนดที่คุณยอมรับเมื่อใช้บริการ", + "data-dimensions-sub-header": "ได้รับข้อมูลเชิงลึกเกี่ยวกับมิติคุณภาพข้อมูลที่สำคัญ—เช่น ความถูกต้อง ความสอดคล้อง และความสมบูรณ์—เพื่อประเมินความครบถ้วนและความน่าเชื่อถือของข้อมูลของคุณ", + "data-health-sub-header": "ติดตามความครอบคลุม คุณภาพ และผลการทดสอบของสินทรัพย์ข้อมูลของคุณ เพื่อตรวจสอบสุขภาพข้อมูลโดยรวมและระบุพื้นที่ที่ต้องปรับปรุง", "data-insight-alert-destination-description": "ส่งการแจ้งเตือนทางอีเมลไปยังผู้ดูแลระบบหรือทีม", "data-insight-alert-trigger-description": "กระตุ้นสำหรับเวลาจริงหรือตั้งเวลาให้ทำทุกวัน, ทุกสัปดาห์ หรือทุกเดือน", "data-insight-message": "จัดการท่อข้อมูลเชิงลึก", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "Execution history and test suite will be preserved", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "ในฐานข้อมูลนี้", + "incident-metrics-sub-header": "ติดตามปริมาณ การตอบสนอง และประสิทธิภาพการแก้ไขอุบัติการณ์ที่เกี่ยวข้องกับข้อมูล เพื่อปรับปรุงความน่าเชื่อถือในการดำเนินงาน", "include-assets-message": "เปิดใช้งานการดึง {{assets}} จากแหล่งข้อมูล", "include-database-filter-extra-information": "ฐานข้อมูลที่ถูกเพิ่มขณะสร้างบริการ", "include-lineage-message": "การตั้งค่าเพื่อปิดการดึงลำดับชั้นจากท่อ", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "เข้าถึงมุมมองรวมศูนย์ของสุขภาพชุดข้อมูลของคุณตามการตรวจสอบทดสอบที่กำหนดค่าไว้", "test-case-name-validation": "ชื่อไม่สามารถมีเครื่องหมายโคลอนคู่ (::) เครื่องหมายคำพูด (\") หรือเครื่องหมายมากกว่า (>) ได้", "test-case-schedule-description": "การทดสอบคุณภาพข้อมูลสามารถกำหนดเวลาให้ดำเนินการในความถี่ที่ต้องการ เขตเวลาคือ UTC", + "test-case-status-sub-header": "แสดงภาพสุขภาพของการทดสอบการตรวจสอบข้อมูลของคุณ พร้อมรายละเอียดของเคสที่สำเร็จ ล้มเหลว และยกเลิก", "test-connection-cannot-be-triggered": "ไม่สามารถกระตุ้นการทดสอบการเชื่อมต่อได้", "test-connection-taking-too-long": { "default": "โปรดตรวจสอบว่าได้กำหนดค่าของ {{service_type}} ให้อนุญาตการเชื่อมต่อแล้ว", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json index 9c46f3807965..2e8e3313ea48 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json @@ -451,10 +451,13 @@ "data-contract-plural": "Veri Sözleşmeleri", "data-contract-status": "Data Contract Status", "data-count-plural": "Veri Sayımları", + "data-dimensions": "Veri Boyutları", "data-discovery": "Veri Keşfi", "data-distribution": "Veri Dağılımı", "data-entity": "Veri {{entity}}", + "data-health": "Veri Sağlığı", "data-health-by-entity": "{{entity}}'e göre veri sağlığı", + "data-health-overview": "Data health overview", "data-insight": "Veri Analizi", "data-insight-active-user-summary": "En Aktif Kullanıcılar", "data-insight-chart": "Veri Analizi Grafiği", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "Pasif Duyurular", "incident": "Olay", "incident-manager": "Olay Yöneticisi", + "incident-metrics": "Olay Metrikleri", "incident-plural": "Olaylar", "incident-status": "Olay Durumu", "include": "Dahil Et", @@ -1999,6 +2003,7 @@ "test-case-plural": "Test Senaryoları", "test-case-resolution-status": "Test Durumu Çözüm Durumu", "test-case-result": "Test Senaryosu Sonuçları", + "test-case-status": "Test Durumu", "test-definition": "Test Definition", "test-definition-plural": "Test Definitions", "test-email": "Test E-postası", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "Güvenlik ve erişim politikası beklentileri", "data-contract-sla-description": "Hizmet Seviyesi Anlaşması Beklentileri", "data-contract-terms-of-service-description": "Bir hizmeti kullanırken kabul ettiğiniz kurallar", + "data-dimensions-sub-header": "Verilerinizin eksiksizliğini ve güvenilirliğini değerlendirmek için doğruluk, tutarlılık ve bütünlük gibi temel veri kalitesi boyutları hakkında içgörüler edinin.", + "data-health-sub-header": "Genel veri sağlığını izlemek ve iyileştirme alanlarını belirlemek için veri varlıklarınızın kapsama alanını, kalitesini ve test sonuçlarını takip edin.", "data-insight-alert-destination-description": "Yöneticilere veya takımlara e-posta bildirimleri gönderin.", "data-insight-alert-trigger-description": "Gerçek zamanlı olarak tetikleyin veya günlük, haftalık ya da aylık olarak zamanlayın.", "data-insight-message": "Veri analizleri iş akışını yönetin.", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "Execution history and test suite will be preserved", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "Bu Veritabanında", + "incident-metrics-sub-header": "Operasyonel güvenilirliği artırmak için veriyle ilgili olayların hacmini, yanıt hızını ve çözüm etkinliğini takip edin.", "include-assets-message": "Veri kaynağından {{assets}} çıkarımını etkinleştirin.", "include-database-filter-extra-information": "Servis oluşturulurken eklenen veritabanı.", "include-lineage-message": "İş akışlarından veri soyu alımını kapatma yapılandırması.", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "Yapılandırılmış test doğrulamalarına dayalı olarak veri setinizin sağlığının merkezi görünümüne erişin.", "test-case-name-validation": "Ad çift iki nokta (::), tırnak işareti (\") veya büyüktür simgesi (>) içeremez.", "test-case-schedule-description": "Veri kalitesi testleri istenen sıklıkta çalışacak şekilde zamanlanabilir. Saat dilimi UTC'dir.", + "test-case-status-sub-header": "Başarılı, başarısız ve iptal edilen vakaların dökümüyle veri doğrulama testlerinizin durumunu görselleştirin.", "test-connection-cannot-be-triggered": "Bağlantı testi tetiklenemez.", "test-connection-taking-too-long": { "default": "Lütfen {{service_type}}'in bağlantılara izin verecek şekilde yapılandırıldığından emin olun", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json index 5e7f4a86e57c..547e38d59c4b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json @@ -451,10 +451,13 @@ "data-contract-plural": "数据协定", "data-contract-status": "数据合同状态", "data-count-plural": "数据计数", + "data-dimensions": "Data Dimensions", "data-discovery": "数据发现", "data-distribution": "数据分发", "data-entity": "数据{{entity}}", + "data-health": "数据健康", "data-health-by-entity": "{{entity}} 的数据健康状况", + "data-health-overview": "Data health overview", "data-insight": "数据洞察", "data-insight-active-user-summary": "最活跃用户", "data-insight-chart": "数据洞察图表", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "不活动的公告", "incident": "事件", "incident-manager": "事件管理", + "incident-metrics": "事件指标", "incident-plural": "事件", "incident-status": "事件状态", "include": "包括", @@ -1999,6 +2003,7 @@ "test-case-plural": "测试用例", "test-case-resolution-status": "测试用例解决状态", "test-case-result": "测试用例结果", + "test-case-status": "测试用例状态", "test-definition": "测试定义", "test-definition-plural": "测试定义", "test-email": "测试邮箱", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "安全和访问策略期望", "data-contract-sla-description": "服务级别协议期望", "data-contract-terms-of-service-description": "使用服务时您同意的规则", + "data-dimensions-sub-header": "深入了解关键数据质量维度——如准确性、一致性和完整性——以评估数据的完整性和可靠性。", + "data-health-sub-header": "跟踪数据资产的覆盖率、质量和测试结果,以监控整体数据健康状况并识别改进领域。", "data-insight-alert-destination-description": "发送通知邮件给管理员或团队", "data-insight-alert-trigger-description": "可以选择实时触发也可以按照每天、每周、每月进行任务调度。", "data-insight-message": "管理数据洞察工作流", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "将保留执行历史记录和测试套件", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "在此数据库中", + "incident-metrics-sub-header": "跟踪数据相关事件的数量、响应能力和解决效率,以提高操作可靠性。", "include-assets-message": "启用从数据源提取{{assets}}", "include-database-filter-extra-information": "在创建服务时添加的数据库", "include-lineage-message": "配置以关闭从工作流提取血缘信息", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "基于配置的测试验证,访问数据集健康状况的集中视图。", "test-case-name-validation": "名称不能包含双冒号(::)、引号(\")或大于符号(>)。", "test-case-schedule-description": "数据质控测试可按所需频率安排运行, 时区为 UTC", + "test-case-status-sub-header": "通过成功、失败和中止用例的明细分解,可视化您的数据验证测试的健康状况。", "test-connection-cannot-be-triggered": "连接测试无法被触发", "test-connection-taking-too-long": { "default": "请确保 {{service_type}} 已配置为允许连接", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json index 7ddee842a8ad..329439ad8318 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json @@ -451,10 +451,13 @@ "data-contract-plural": "資料合約", "data-contract-status": "資料合約狀態", "data-count-plural": "資料計數", + "data-dimensions": "資料維度", "data-discovery": "資料探索", "data-distribution": "資料分佈", "data-entity": "資料 {{entity}}", + "data-health": "資料健康", "data-health-by-entity": "依 {{entity}} 的資料健康度", + "data-health-overview": "Data health overview", "data-insight": "資料洞察", "data-insight-active-user-summary": "最活躍使用者", "data-insight-chart": "資料洞察圖表", @@ -977,6 +980,7 @@ "inactive-announcement-plural": "非作用中公告", "incident": "事件", "incident-manager": "事件管理員", + "incident-metrics": "事件指標", "incident-plural": "事件", "incident-status": "事件狀態", "include": "包含", @@ -1999,6 +2003,7 @@ "test-case-plural": "測試案例", "test-case-resolution-status": "測試案例解決狀態", "test-case-result": "測試案例結果", + "test-case-status": "測試案例狀態", "test-definition": "Test Definition", "test-definition-plural": "Test Definitions", "test-email": "測試電子郵件", @@ -2399,6 +2404,8 @@ "data-contract-security-description": "關於安全性和訪問政策的期望", "data-contract-sla-description": "服務水準協議期望", "data-contract-terms-of-service-description": "使用服務時您同意的規則", + "data-dimensions-sub-header": "深入了解關鍵資料品質維度——如準確性、一致性和完整性——以評估資料的完整性和可靠性。", + "data-health-sub-header": "追蹤資料資產的覆蓋率、品質和測試結果,以監控整體資料健康狀況並識別改進領域。", "data-insight-alert-destination-description": "將電子郵件通知傳送給管理員或團隊。", "data-insight-alert-trigger-description": "即時觸發或排程每日、每週或每月觸發。", "data-insight-message": "管理資料洞察管線。", @@ -2610,6 +2617,7 @@ "import-odcs-replace-preserve-history": "Execution history and test suite will be preserved", "import-odcs-replace-preserve-id": "Contract ID, FQN, and entity reference will be preserved", "in-this-database": "在此資料庫中", + "incident-metrics-sub-header": "追蹤資料相關事件的數量、響應能力和解決效率,以提高操作可靠性。", "include-assets-message": "啟用從資料來源擷取 {{assets}}。", "include-database-filter-extra-information": "建立服務時新增的資料庫。", "include-lineage-message": "關閉從管線擷取血緣的組態。", @@ -3038,6 +3046,7 @@ "test-case-insight-description": "根據設定的測試驗證,存取您資料集的健康狀況集中檢視。", "test-case-name-validation": "名稱不能包含雙冒號(::)、引號(\")或大於符號(>)。", "test-case-schedule-description": "可以排程資料品質測試以所需的頻率執行。時區為 UTC。", + "test-case-status-sub-header": "透過成功、失敗和中止案例的明細分解,視覺化您的資料驗證測試的健康狀況。", "test-connection-cannot-be-triggered": "無法觸發測試連線。", "test-connection-taking-too-long": { "default": "請確保 {{service_type}} 已設定為允許連線", diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityClassBase.ts index 852cfefd5918..2bdadb32aed4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataQuality/DataQualityClassBase.ts @@ -11,7 +11,9 @@ * limitations under the License. */ import { ReactComponent as TestCaseIcon } from '../../assets/svg/all-activity-v2.svg'; +import { ReactComponent as DashboardIcon } from '../../assets/svg/ic-dashboard.svg'; import { ReactComponent as TestSuiteIcon } from '../../assets/svg/icon-test-suite.svg'; +import DataQualityDashboard from '../../components/DataQuality/DataQualityDashboard/DataQualityDashboard.component'; import { TestCases } from '../../components/DataQuality/TestCases/TestCases.component'; import { TestSuites } from '../../components/DataQuality/TestSuite/TestSuiteList/TestSuites.component'; import i18n from '../../utils/i18next/LocalUtil'; @@ -29,6 +31,14 @@ export type DataQualityLeftSideBarType = { class DataQualityClassBase { public getLeftSideBar(): DataQualityLeftSideBarType[] { return [ + { + key: DataQualityPageTabs.DASHBOARD, + id: 'dashboard', + label: i18n.t('label.summary'), + icon: DashboardIcon, + description: i18n.t('label.data-health-overview'), + iconProps: { className: 'side-panel-icons' }, + }, { key: DataQualityPageTabs.TEST_CASES, label: i18n.t('label.by-entity', { @@ -62,6 +72,11 @@ class DataQualityClassBase { public getDataQualityTab() { return [ + { + component: DataQualityDashboard, + key: DataQualityPageTabs.DASHBOARD, + label: i18n.t('label.summary'), + }, { key: DataQualityPageTabs.TEST_CASES, component: TestCases, @@ -76,7 +91,7 @@ class DataQualityClassBase { } public getDefaultActiveTab(): DataQualityPageTabs { - return DataQualityPageTabs.TEST_CASES; + return DataQualityPageTabs.DASHBOARD; } public getExportDataQualityDashboardButton( diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.test.tsx index bf34b970b913..b1d4f4edff51 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.test.tsx @@ -12,10 +12,12 @@ */ import { render, waitFor } from '@testing-library/react'; +import { EntityTabs } from '../../enums/entity.enum'; import { useFqn } from '../../hooks/useFqn'; import { searchQuery } from '../../rest/searchAPI'; import { getTagByFqn } from '../../rest/tagAPI'; import tagClassBase from '../../utils/TagClassBase'; +import { useRequiredParams } from '../../utils/useRequiredParams'; import TagPage from './TagPage'; jest.mock('../../hooks/useCustomPages', () => ({ @@ -66,6 +68,24 @@ jest.mock('../../hooks/useFqn', () => ({ useFqn: jest.fn(), })); +jest.mock('../../utils/useRequiredParams', () => ({ + useRequiredParams: jest.fn().mockReturnValue({}), +})); + +const mockDataQualityDashboard = jest.fn(); + +jest.mock( + '../../components/DataQuality/DataQualityDashboard/DataQualityDashboard.component', + () => ({ + __esModule: true, + default: (props: unknown) => { + mockDataQualityDashboard(props); + + return
; + }, + }) +); + const mockNavigate = jest.fn(); jest.mock('react-router-dom', () => ({ @@ -184,6 +204,7 @@ describe('TagPage', () => { (tagClassBase.getAdditionalTagDetailPageTabs as jest.Mock).mockReturnValue( [] ); + (useRequiredParams as jest.Mock).mockReturnValue({}); }); it('should call getAdditionalTagDetailPageTabs with the fetched tag', async () => { @@ -231,4 +252,66 @@ describe('TagPage', () => { expect(searchQuery).toHaveBeenCalled(); }); }); + + describe('DATA_OBSERVABILITY tab', () => { + beforeEach(() => { + (useFqn as jest.Mock).mockReturnValue({ fqn: 'PII.NonSensitive' }); + (useRequiredParams as jest.Mock).mockReturnValue({ + tab: EntityTabs.DATA_OBSERVABILITY, + }); + mockDataQualityDashboard.mockClear(); + }); + + it('renders DataQualityDashboard when DQ tab is active', async () => { + const { getByTestId } = render(); + + await waitFor(() => { + expect(getByTestId('dq-dashboard')).toBeInTheDocument(); + }); + }); + + it('passes isGovernanceView as true to DataQualityDashboard', async () => { + render(); + + await waitFor(() => { + expect(mockDataQualityDashboard).toHaveBeenCalledWith( + expect.objectContaining({ isGovernanceView: true }) + ); + }); + }); + + it('passes hiddenFilters containing tags to DataQualityDashboard', async () => { + render(); + + await waitFor(() => { + expect(mockDataQualityDashboard).toHaveBeenCalledWith( + expect.objectContaining({ hiddenFilters: expect.arrayContaining(['tags']) }) + ); + }); + }); + + it('passes className as data-quality-governance-tab-wrapper to DataQualityDashboard', async () => { + render(); + + await waitFor(() => { + expect(mockDataQualityDashboard).toHaveBeenCalledWith( + expect.objectContaining({ + className: 'data-quality-governance-tab-wrapper', + }) + ); + }); + }); + + it('passes initialFilters with tag fqn when tag has a fullyQualifiedName', async () => { + render(); + + await waitFor(() => { + expect(mockDataQualityDashboard).toHaveBeenCalledWith( + expect.objectContaining({ + initialFilters: { tags: ['PII.NonSensitive'] }, + }) + ); + }); + }); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.tsx index 90260b7f6419..d1bf12981c6b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/TagPage.tsx @@ -46,10 +46,12 @@ import ResizablePanels from '../../components/common/ResizablePanels/ResizablePa import StatusBadge from '../../components/common/StatusBadge/StatusBadge.component'; import { StatusType } from '../../components/common/StatusBadge/StatusBadge.interface'; import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; +import { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; import { TitleBreadcrumbProps } from '../../components/common/TitleBreadcrumb/TitleBreadcrumb.interface'; import { GenericProvider } from '../../components/Customization/GenericProvider/GenericProvider'; import { GenericTab } from '../../components/Customization/GenericTab/GenericTab'; import { AssetSelectionModal } from '../../components/DataAssets/AssetsSelectionModal/AssetSelectionModal'; +import DataQualityDashboard from '../../components/DataQuality/DataQualityDashboard/DataQualityDashboard.component'; import { EntityHeader } from '../../components/Entity/EntityHeader/EntityHeader.component'; import { EntityStatusBadge } from '../../components/Entity/EntityStatusBadge/EntityStatusBadge.component'; import EntitySummaryPanel from '../../components/Explore/EntitySummaryPanel/EntitySummaryPanel.component'; @@ -79,7 +81,11 @@ import { ResourceEntity, } from '../../context/PermissionProvider/PermissionProvider.interface'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; -import { EntityType, TabSpecificField } from '../../enums/entity.enum'; +import { + EntityTabs, + EntityType, + TabSpecificField, +} from '../../enums/entity.enum'; import { SearchIndex } from '../../enums/search.enum'; import { ProviderType, Tag } from '../../generated/entity/classification/tag'; import { EntityStatus } from '../../generated/entity/data/glossaryTerm'; @@ -501,27 +507,28 @@ const TagPage = () => { return []; } - const items: Array<{ - label: JSX.Element; - key: string; - children?: JSX.Element; - isHidden?: boolean; - }> = [ + const tabs: TabProps[] = [ { - label: , - key: 'overview', + label: ( + + ), + key: EntityTabs.OVERVIEW, children: , }, { label: ( ), - key: 'assets', + key: EntityTabs.ASSETS, children: ( { label: ( ), - key: TagTabs.ACTIVITY_FEED, + key: EntityTabs.ACTIVITY_FEED, children: ( { /> ), }, + { + key: EntityTabs.DATA_OBSERVABILITY, + label: ( + + ), + children: ( +
+ +
+ ), + }, ]; - items.push( + tabs.push( ...tagClassBase.getAdditionalTagDetailPageTabs(tagItem, activeTab) ); - return items; + return tabs; }, [ tagItem, - previewAsset, activeTab, assetCount, feedCount, - assetTabRef, + previewAsset, + isCertificationClassification, + haveAssetEditPermission, handleAssetSave, - editTagsPermission, + handleAssetClick, + handleFeedCount, + t, ]); const icon = useMemo(() => { if (tagItem?.style?.iconURL) { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/tag-page.less b/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/tag-page.less index 75fe8ceb738b..c2a71db237eb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/tag-page.less +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TagPage/tag-page.less @@ -21,3 +21,7 @@ background-color: transparent; } } + +.tag-page-dq-tab-pane { + height: @tag-page-height; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/variables.less b/openmetadata-ui/src/main/resources/ui/src/styles/variables.less index 7b7db593d7c8..cc3d9f35c911 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/variables.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/variables.less @@ -49,6 +49,7 @@ @green-15: #f7ffe7; @green-16: #abefc6; @green-17: #067647; +@green-100: #dcfae6; @green-600: #079455; @yellow-1: #fbf2db; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DataQuality/DataQualityUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DataQuality/DataQualityUtils.tsx index 7bc70de300c8..b9607ce685b1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DataQuality/DataQualityUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DataQuality/DataQualityUtils.tsx @@ -12,6 +12,7 @@ */ import { t } from 'i18next'; import { + cloneDeep, isArray, isNil, isUndefined, @@ -22,6 +23,7 @@ import { startCase, uniqBy, } from 'lodash'; +import QueryString from 'qs'; import { Surface } from 'recharts'; import { ReactComponent as AccuracyIcon } from '../../assets/svg/ic-accuracy.svg'; import { ReactComponent as ColumnIcon } from '../../assets/svg/ic-column.svg'; @@ -34,11 +36,15 @@ import { ReactComponent as UniquenessIcon } from '../../assets/svg/ic-uniqueness import { ReactComponent as ValidityIcon } from '../../assets/svg/ic-validity.svg'; import { ReactComponent as NoDimensionIcon } from '../../assets/svg/no-dimension-icon.svg'; import { SelectionOption } from '../../components/common/SelectionCardGroup/SelectionCardGroup.interface'; +import { StatusData } from '../../components/DataQuality/ChartWidgets/StatusCardWidget/StatusCardWidget.interface'; import { TestCaseSearchParams } from '../../components/DataQuality/DataQuality.interface'; import { SearchDropdownOption } from '../../components/SearchDropdown/SearchDropdown.interface'; +import { TEXT_GREY_MUTED } from '../../constants/constants'; +import { DEFAULT_DIMENSIONS_DATA } from '../../constants/DataQuality.constants'; import { TEST_CASE_FILTERS } from '../../constants/profiler.constant'; import { TestCaseType } from '../../enums/TestSuite.enum'; import { Table } from '../../generated/entity/data/table'; +import { TestCaseStatus } from '../../generated/entity/feed/testCaseResult'; import { DataQualityReport } from '../../generated/tests/dataQualityReport'; import { TestCase, @@ -51,13 +57,17 @@ import { } from '../../generated/tests/testDefinition'; import { DataInsightChartTooltipProps } from '../../interface/data-insight.interface'; import { TableSearchSource } from '../../interface/search.interface'; -import { DataQualityDashboardChartFilters } from '../../pages/DataQuality/DataQualityPage.interface'; +import { + DataQualityDashboardChartFilters, + DataQualityPageTabs, +} from '../../pages/DataQuality/DataQualityPage.interface'; import { ListTestCaseParamsBySearch } from '../../rest/testAPI'; import { getEntryFormattedValue } from '../DataInsightUtils'; import { formatDate } from '../date-time/DateTimeUtils'; import EntityLink from '../EntityLink'; import { getColumnNameFromEntityLink } from '../EntityUtils'; import { getEntityFQN } from '../FeedUtils'; +import { getDataQualityPagePath } from '../RouterUtils'; import { generateEntityLink } from '../TableUtils'; /** @@ -678,3 +688,75 @@ export function getColumnNameFromColumnFilterKey( ? columnFilterKey.slice(columnFilterKey.lastIndexOf('::') + 2) : columnFilterKey; } + +/** Returns path and search for navigating to the Test Cases tab with a status filter. */ +export const getTestCaseTabPath = (testCaseStatus: TestCaseStatus) => ({ + pathname: getDataQualityPagePath(DataQualityPageTabs.TEST_CASES), + search: QueryString.stringify({ testCaseStatus }), +}); + +export const transformToTestCaseStatusByDimension = ( + inputData: DataQualityReport['data'] +): StatusData[] => { + const result: { [key: string]: StatusData } = cloneDeep( + DEFAULT_DIMENSIONS_DATA + ); + + inputData.forEach((item) => { + const { + document_count, + 'testCaseResult.testCaseStatus': status, + dataQualityDimension = DataQualityDimensions.NoDimension, + } = item; + const count = parseInt(document_count, 10); + + if (!result[dataQualityDimension]) { + result[dataQualityDimension] = { + title: dataQualityDimension, + success: 0, + failed: 0, + aborted: 0, + total: 0, + }; + } + + if (status === 'success') { + result[dataQualityDimension].success += count; + } else if (status === 'failed') { + result[dataQualityDimension].failed += count; + } else if (status === 'aborted') { + result[dataQualityDimension].aborted += count; + } + + result[dataQualityDimension].total += count; + }); + + return Object.values(result); +}; + +export const getPieChartLabel = (label: string, value = 0) => { + return ( + <> + + {value} + + + {label} + + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.test.ts new file mode 100644 index 000000000000..8a11e22a62d9 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.test.ts @@ -0,0 +1,156 @@ +/* + * Copyright 2025 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 { createElement } from 'react'; +import { EntityTabs } from '../../enums/entity.enum'; +import domainClassBase, { + DomainClassBase, + DomainDetailPageTabProps, +} from './DomainClassBase'; + +jest.mock('../../constants/Domain.constants', () => ({ + DOMAIN_DUMMY_DATA: {}, +})); + +jest.mock('../DomainUtils', () => ({ + getDomainDetailTabs: jest + .fn() + .mockReturnValue([{ key: EntityTabs.DOCUMENTATION, children: null }]), + getDomainWidgetsFromKey: jest.fn().mockReturnValue([]), +})); + +jest.mock( + '../../components/DataQuality/DataQualityDashboard/DataQualityDashboard.component', + () => ({ __esModule: true, default: () => null }) +); + +jest.mock('../../components/common/TabsLabel/TabsLabel.component', () => ({ + __esModule: true, + default: () => null, +})); + +jest.mock('../i18next/LocalUtil', () => ({ + __esModule: true, + default: { t: jest.fn((key: string) => key) }, +})); + +jest.mock('../CustomizePage/CustomizePageUtils', () => ({ + getTabLabelFromId: jest.fn((tab: string) => tab), +})); + +const mockProps = { + domain: { fullyQualifiedName: 'Finance' }, + isVersionsView: false, +} as unknown as DomainDetailPageTabProps; + +describe('DomainClassBase', () => { + let instance: DomainClassBase; + + beforeEach(() => { + jest.clearAllMocks(); + instance = new DomainClassBase(); + }); + + describe('getDomainDetailPageTabs', () => { + it('in non-version view appends DATA_OBSERVABILITY tab after base tabs', () => { + const tabs = instance.getDomainDetailPageTabs(mockProps); + + expect(tabs.at(-1)?.key).toBe(EntityTabs.DATA_OBSERVABILITY); + }); + + it('in version view returns only base tabs without DATA_OBSERVABILITY', () => { + const props = { ...mockProps, isVersionsView: true }; + const tabs = instance.getDomainDetailPageTabs(props); + const dqTab = tabs.find((t) => t.key === EntityTabs.DATA_OBSERVABILITY); + + expect(dqTab).toBeUndefined(); + }); + + it('DQ tab passes isGovernanceView as true', () => { + const tabs = instance.getDomainDetailPageTabs(mockProps); + const dqTab = tabs.at(-1); + const childProps = (dqTab?.children as ReturnType) + .props; + + expect(childProps.isGovernanceView).toBe(true); + }); + + it('DQ tab passes domain.fullyQualifiedName as initialFilters.domainFqn', () => { + const tabs = instance.getDomainDetailPageTabs(mockProps); + const childProps = ( + tabs.at(-1)?.children as ReturnType + ).props; + + expect(childProps.initialFilters?.domainFqn).toBe('Finance'); + }); + + it('DQ tab passes undefined initialFilters when domain.fullyQualifiedName is absent', () => { + const props = { + ...mockProps, + domain: { fullyQualifiedName: undefined }, + } as unknown as DomainDetailPageTabProps; + const tabs = instance.getDomainDetailPageTabs(props); + const childProps = ( + tabs.at(-1)?.children as ReturnType + ).props; + + expect(childProps.initialFilters).toBeUndefined(); + }); + + it('DQ tab passes className as data-quality-governance-tab-wrapper tw:mt-2', () => { + const tabs = instance.getDomainDetailPageTabs(mockProps); + const childProps = ( + tabs.at(-1)?.children as ReturnType + ).props; + + expect(childProps.className).toBe( + 'data-quality-governance-tab-wrapper tw:mt-2' + ); + }); + }); + + describe('getDomainDetailPageTabsIds', () => { + it('includes DATA_OBSERVABILITY tab ID', () => { + const tabs = instance.getDomainDetailPageTabsIds(); + const dqTab = tabs.find((t) => t.id === EntityTabs.DATA_OBSERVABILITY); + + expect(dqTab).toBeDefined(); + }); + + it('DATA_OBSERVABILITY tab is not editable', () => { + const tabs = instance.getDomainDetailPageTabsIds(); + const dqTab = tabs.find((t) => t.id === EntityTabs.DATA_OBSERVABILITY); + + expect(dqTab?.editable).toBe(false); + }); + + it('DATA_OBSERVABILITY tab has empty layout', () => { + const tabs = instance.getDomainDetailPageTabsIds(); + const dqTab = tabs.find((t) => t.id === EntityTabs.DATA_OBSERVABILITY); + + expect(dqTab?.layout).toEqual([]); + }); + + it('DATA_OBSERVABILITY is the last tab ID', () => { + const tabs = instance.getDomainDetailPageTabsIds(); + + expect(tabs.at(-1)?.id).toBe(EntityTabs.DATA_OBSERVABILITY); + }); + }); + + describe('singleton export', () => { + it('default export is an instance of DomainClassBase', () => { + expect(domainClassBase).toBeInstanceOf(DomainClassBase); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts index db0f1afbb195..7bd557115c1c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Domain/DomainClassBase.ts @@ -11,7 +11,10 @@ * limitations under the License. */ +import { createElement } from 'react'; +import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; import { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; +import DataQualityDashboard from '../../components/DataQuality/DataQualityDashboard/DataQualityDashboard.component'; import { DataProductsTabRef } from '../../components/Domain/DomainTabs/DataProductsTab/DataProductsTab.interface'; import { EntityDetailsObjectInterface } from '../../components/Explore/ExplorePage.interface'; import { AssetsTabRef } from '../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.component'; @@ -89,7 +92,28 @@ class DomainClassBase { public getDomainDetailPageTabs( domainDetailsPageProps: DomainDetailPageTabProps ): TabProps[] { - return getDomainDetailTabs(domainDetailsPageProps); + const baseTabs = getDomainDetailTabs(domainDetailsPageProps); + + if (domainDetailsPageProps.isVersionsView) { + return baseTabs; + } + + const dqTab: TabProps = { + label: createElement(TabsLabel, { + id: EntityTabs.DATA_OBSERVABILITY, + name: i18n.t('label.data-observability'), + }), + key: EntityTabs.DATA_OBSERVABILITY, + children: createElement(DataQualityDashboard, { + isGovernanceView: true, + className: 'data-quality-governance-tab-wrapper tw:mt-2', + initialFilters: domainDetailsPageProps.domain.fullyQualifiedName + ? { domainFqn: domainDetailsPageProps.domain.fullyQualifiedName } + : undefined, + }), + }; + + return [...baseTabs, dqTab]; } public getDomainDetailPageTabsIds(): Tab[] { @@ -100,6 +124,7 @@ class DomainClassBase { EntityTabs.ACTIVITY_FEED, EntityTabs.ASSETS, EntityTabs.CUSTOM_PROPERTIES, + EntityTabs.DATA_OBSERVABILITY, ].map((tab: EntityTabs) => ({ id: tab, name: tab, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Glossary/GlossaryTermClassBase.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Glossary/GlossaryTermClassBase.test.ts index aed33edc2a91..4c83048228a1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Glossary/GlossaryTermClassBase.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Glossary/GlossaryTermClassBase.test.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import React from 'react'; import { EntityTabs } from '../../enums/entity.enum'; import glossaryTermClassBase, { GlossaryTermClassBase, @@ -28,7 +29,25 @@ jest.mock('../../utils/CustomizePage/CustomizePageUtils', () => ({ getTabLabelFromId: jest.fn((tab: string) => tab), })); -const mockProps = {} as GlossaryTermDetailPageTabProps; +jest.mock( + '../../components/DataQuality/DataQualityDashboard/DataQualityDashboard.component', + () => ({ __esModule: true, default: () => null }) +); + +jest.mock('../../components/common/TabsLabel/TabsLabel.component', () => ({ + __esModule: true, + default: () => null, +})); + +jest.mock('../i18next/LocalUtil', () => ({ + __esModule: true, + default: { t: jest.fn((key: string) => key) }, +})); + +const mockProps = { + glossaryTerm: { fullyQualifiedName: 'Finance.Revenue' }, + isVersionView: false, +} as unknown as GlossaryTermDetailPageTabProps; describe('GlossaryTermClassBase', () => { let instance: GlossaryTermClassBase; @@ -40,18 +59,90 @@ describe('GlossaryTermClassBase', () => { describe('getGlossaryTermDetailPageTabs', () => { it('delegates to getGlossaryTermDetailPageTabs util', () => { - const result = instance.getGlossaryTermDetailPageTabs(mockProps); + instance.getGlossaryTermDetailPageTabs(mockProps); expect(getGlossaryTermDetailPageTabs).toHaveBeenCalledWith(mockProps); - expect(result).toEqual([{ key: 'mock-tab' }]); + }); + }); + + describe('getGlossaryTermDetailPageTabs — DATA_OBSERVABILITY tab', () => { + it('appends DATA_OBSERVABILITY tab in non-version view', () => { + const tabs = instance.getGlossaryTermDetailPageTabs(mockProps); + const dqTab = tabs.find((t) => t.key === EntityTabs.DATA_OBSERVABILITY); + + expect(dqTab).toBeDefined(); + }); + + it('DATA_OBSERVABILITY tab is NOT present in version view', () => { + const props = { ...mockProps, isVersionView: true }; + const tabs = instance.getGlossaryTermDetailPageTabs(props); + const dqTab = tabs.find((t) => t.key === EntityTabs.DATA_OBSERVABILITY); + + expect(dqTab).toBeUndefined(); + }); + + it('DQ tab passes isGovernanceView as true', () => { + const tabs = instance.getGlossaryTermDetailPageTabs(mockProps); + const dqTab = tabs.find((t) => t.key === EntityTabs.DATA_OBSERVABILITY); + const childProps = ( + dqTab?.children as React.ReactElement<{ + isGovernanceView: boolean; + hiddenFilters: string[]; + initialFilters?: Record; + }> + ).props; + + expect(childProps.isGovernanceView).toBe(true); + }); + + it('DQ tab passes glossaryTerm.fqn as initialFilters.glossaryTerms', () => { + const tabs = instance.getGlossaryTermDetailPageTabs(mockProps); + const dqTab = tabs.find((t) => t.key === EntityTabs.DATA_OBSERVABILITY); + const childProps = ( + dqTab?.children as React.ReactElement<{ + isGovernanceView: boolean; + hiddenFilters: string[]; + initialFilters?: Record; + }> + ).props; + + expect(childProps.initialFilters?.glossaryTerms).toEqual([ + 'Finance.Revenue', + ]); + }); + + it('DQ tab hides glossaryTerms filter', () => { + const tabs = instance.getGlossaryTermDetailPageTabs(mockProps); + const dqTab = tabs.find((t) => t.key === EntityTabs.DATA_OBSERVABILITY); + const childProps = ( + dqTab?.children as React.ReactElement<{ + isGovernanceView: boolean; + hiddenFilters: string[]; + initialFilters?: Record; + }> + ).props; + + expect(childProps.hiddenFilters).toContain('glossaryTerms'); + }); + + it('DQ tab passes className as data-quality-governance-tab-wrapper', () => { + const tabs = instance.getGlossaryTermDetailPageTabs(mockProps); + const dqTab = tabs.find((t) => t.key === EntityTabs.DATA_OBSERVABILITY); + const childProps = ( + dqTab?.children as React.ReactElement<{ + className?: string; + }> + ).props; + + expect(childProps.className).toBe('data-quality-governance-tab-wrapper'); }); }); describe('getGlossaryTermDetailPageTabsIds', () => { - it('returns all 5 expected tab IDs', () => { + it('returns all 6 expected tab IDs', () => { const tabs = instance.getGlossaryTermDetailPageTabsIds(); - expect(tabs).toHaveLength(5); + expect(tabs).toHaveLength(6); }); it('includes OVERVIEW tab', () => { @@ -114,8 +205,31 @@ describe('GlossaryTermClassBase', () => { EntityTabs.ASSETS, EntityTabs.ACTIVITY_FEED, EntityTabs.CUSTOM_PROPERTIES, + EntityTabs.DATA_OBSERVABILITY, ]); }); + + it('includes DATA_OBSERVABILITY tab ID', () => { + const tabs = instance.getGlossaryTermDetailPageTabsIds(); + + expect( + tabs.find((t) => t.id === EntityTabs.DATA_OBSERVABILITY) + ).toBeDefined(); + }); + + it('DATA_OBSERVABILITY tab is not editable', () => { + const tabs = instance.getGlossaryTermDetailPageTabsIds(); + const dqTab = tabs.find((t) => t.id === EntityTabs.DATA_OBSERVABILITY); + + expect(dqTab?.editable).toBe(false); + }); + + it('DATA_OBSERVABILITY tab has empty layout', () => { + const tabs = instance.getGlossaryTermDetailPageTabsIds(); + const dqTab = tabs.find((t) => t.id === EntityTabs.DATA_OBSERVABILITY); + + expect(dqTab?.layout).toEqual([]); + }); }); describe('singleton export', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Glossary/GlossaryTermClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/Glossary/GlossaryTermClassBase.ts index fa07d59a93a5..e695b69b04eb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Glossary/GlossaryTermClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Glossary/GlossaryTermClassBase.ts @@ -10,8 +10,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import React, { createElement } from 'react'; +import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component'; import { TabProps } from '../../components/common/TabsLabel/TabsLabel.interface'; +import DataQualityDashboard from '../../components/DataQuality/DataQualityDashboard/DataQualityDashboard.component'; import { EntityDetailsObjectInterface } from '../../components/Explore/ExplorePage.interface'; import { AssetsTabRef } from '../../components/Glossary/GlossaryTerms/tabs/AssetsTabs.component'; import { OperationPermission } from '../../context/PermissionProvider/PermissionProvider.interface'; @@ -20,6 +22,7 @@ import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm'; import { Tab } from '../../generated/system/ui/uiCustomization'; import { FeedCounts } from '../../interface/feed.interface'; import { getTabLabelFromId } from '../CustomizePage/CustomizePageUtils'; +import i18n from '../i18next/LocalUtil'; import { getGlossaryTermDetailPageTabs } from './GlossaryTermUtils'; export interface GlossaryTermDetailPageTabProps { @@ -46,7 +49,29 @@ class GlossaryTermClassBase { public getGlossaryTermDetailPageTabs( props: GlossaryTermDetailPageTabProps ): TabProps[] { - return getGlossaryTermDetailPageTabs(props); + const baseTabs = getGlossaryTermDetailPageTabs(props); + + if (props.isVersionView) { + return baseTabs; + } + + const dqTab: TabProps = { + label: createElement(TabsLabel, { + id: EntityTabs.DATA_OBSERVABILITY, + name: i18n.t('label.data-observability'), + }), + key: EntityTabs.DATA_OBSERVABILITY, + children: createElement(DataQualityDashboard, { + isGovernanceView: true, + className: 'data-quality-governance-tab-wrapper', + hiddenFilters: ['glossaryTerms'], + initialFilters: props.glossaryTerm.fullyQualifiedName + ? { glossaryTerms: [props.glossaryTerm.fullyQualifiedName] } + : undefined, + }), + }; + + return [...baseTabs, dqTab]; } public getGlossaryTermDetailPageTabsIds(): Tab[] { @@ -56,6 +81,7 @@ class GlossaryTermClassBase { EntityTabs.ASSETS, EntityTabs.ACTIVITY_FEED, EntityTabs.CUSTOM_PROPERTIES, + EntityTabs.DATA_OBSERVABILITY, ].map((tab: EntityTabs) => ({ id: tab, name: tab, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.test.ts index 6d51d5076064..54fe820a7acd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.test.ts @@ -19,13 +19,27 @@ import { Tag } from '../generated/entity/classification/tag'; import { searchQuery } from '../rest/searchAPI'; import tagClassBase, { TagClassBase } from './TagClassBase'; -jest.mock('../rest/searchAPI'); +jest.mock('../rest/searchAPI', () => ({ + searchQuery: jest.fn(), +})); jest.mock('./StringsUtils', () => ({ getEncodedFqn: jest.fn().mockReturnValue('test'), escapeESReservedCharacters: jest.fn().mockReturnValue('test'), })); +jest.mock('./SearchUtils', () => ({ + getTermQuery: jest.fn((params: Record) => ({ + query: { + bool: { + must: Object.entries(params).map(([key, value]) => ({ + term: { [key]: value }, + })), + }, + }, + })), +})); + jest.mock('./CustomizePage/CustomizePageUtils', () => ({ getTabLabelFromId: jest.fn().mockReturnValue('Tab Label'), })); @@ -42,6 +56,12 @@ jest.mock('../components/DataAssets/OwnerLabelV2/OwnerLabelV2', () => ({ OwnerLabelV2: 'OwnerLabelV2', })); +jest.mock('./i18next/LocalUtil', () => ({ + __esModule: true, + default: { t: jest.fn((key: string) => key) }, + t: jest.fn((key: string) => key), +})); + const mockSearchResponse = (fqn: string) => ({ hits: { hits: [{ _source: { fullyQualifiedName: fqn } }], @@ -129,10 +149,10 @@ describe('TagClassBase', () => { }); describe('getTagDetailPageTabsIds', () => { - it('returns 4 tabs', () => { + it('returns 5 tabs', () => { const tabs = tagClassBase.getTagDetailPageTabsIds(); - expect(tabs).toHaveLength(4); + expect(tabs).toHaveLength(5); }); it('returns tabs in expected order', () => { @@ -144,6 +164,7 @@ describe('TagClassBase', () => { EntityTabs.ASSETS, EntityTabs.ACTIVITY_FEED, EntityTabs.CUSTOM_PROPERTIES, + EntityTabs.DATA_OBSERVABILITY, ]); }); @@ -162,6 +183,25 @@ describe('TagClassBase', () => { .filter((t) => t.id !== EntityTabs.OVERVIEW) .forEach((t) => expect(t.editable).toBe(false)); }); + + it('includes DATA_OBSERVABILITY tab with empty layout', () => { + const tabs = tagClassBase.getTagDetailPageTabsIds(); + const dqTab = tabs.find((t) => t.id === EntityTabs.DATA_OBSERVABILITY); + + expect(dqTab).toBeDefined(); + expect(dqTab?.layout).toEqual([]); + expect(dqTab?.editable).toBe(false); + }); + + it('DATA_OBSERVABILITY tab comes after all base tabs', () => { + const tabs = tagClassBase.getTagDetailPageTabsIds(); + const overviewIndex = tabs.findIndex((t) => t.id === EntityTabs.OVERVIEW); + const dqIndex = tabs.findIndex( + (t) => t.id === EntityTabs.DATA_OBSERVABILITY + ); + + expect(overviewIndex).toBeLessThan(dqIndex); + }); }); describe('getDefaultLayout', () => { @@ -381,23 +421,13 @@ describe('TagClassBase', () => { }); describe('getAdditionalTagDetailPageTabs', () => { - it('returns an empty array by default', () => { - const mockTag = { fullyQualifiedName: 'PII.Sensitive' } as unknown as Tag; - - expect( - tagClassBase.getAdditionalTagDetailPageTabs(mockTag, 'overview') - ).toEqual([]); - }); - - it('returns an empty array regardless of activeTab value', () => { - const mockTag = { fullyQualifiedName: 'PII.Sensitive' } as unknown as Tag; + it('returns empty array in base class', () => { + const tabs = tagClassBase.getAdditionalTagDetailPageTabs( + { fullyQualifiedName: 'PII.Sensitive' } as unknown as Tag, + 'overview' + ); - expect( - tagClassBase.getAdditionalTagDetailPageTabs(mockTag, 'assets') - ).toEqual([]); - expect( - tagClassBase.getAdditionalTagDetailPageTabs(mockTag, 'activity_feed') - ).toEqual([]); + expect(tabs).toEqual([]); }); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.ts index 175d09a6bb50..65f0113aa29b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TagClassBase.ts @@ -136,6 +136,7 @@ class TagClassBase { EntityTabs.ASSETS, EntityTabs.ACTIVITY_FEED, EntityTabs.CUSTOM_PROPERTIES, + EntityTabs.DATA_OBSERVABILITY, ].map((tab: EntityTabs) => ({ id: tab, name: tab,