From b0ff7887ae917c39e5c4c3d40fe29dae1f0ff6be Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Thu, 29 May 2025 17:54:06 +0530 Subject: [PATCH] Feat: #21138 Display Test Case Version Change (#21324) * fix: #21138 Display Test Case Version Change * Enhance Test Case Version Page: Integrate versioning functionality and update interfaces. Added API calls for fetching test case versions and details, and updated the DataAssetsVersionHeader interface to include TestCase. Improved loading states and error handling in the TestCaseVersionPage component. * Enhance Incident Manager with version handling and permissions - Added support for version pages in IncidentManagerPageHeader and TestCaseResultTab components. - Introduced isVersionPage prop to manage permissions and display logic based on version context. - Updated IncidentManagerDetailPage to handle version-specific data fetching and display. - Refactored related components to utilize ChangeDescription for improved data handling. * Implement parameter value diff display in TestCaseResultTab - Added a new utility function to compute differences in parameter values. - Integrated the parameter value diff display into the TestCaseResultTab component. - Updated Feeds.constants to include PARAMETER_VALUES enum. - Enhanced EntityVersionUtils with new methods for handling parameter value diffs. * Enhance Data Quality components with parameter value diff display and styling updates - Updated EditTestCaseModal to conditionally display test case names. - Modified IncidentManagerPageHeader to disable compact view. - Added new styles for version SQL expression display in TestCaseResultTab. - Refactored TestCaseResultTab to handle parameter value diffs more effectively, including separate rendering for SQL expressions. - Enhanced utility functions in EntityVersionUtils to support default values in parameter diffs. - Updated DataQualityUtils to use isNil for better null handling. * added licence * updated edit icon and hide incident tab in version view * Enhance Test Case Versioning: Update routing to support detailed version paths and improve UI styles for version display. Added new route for test case details with version and adjusted related components for better handling of version-specific tabs. * fix unit test * added playwright * resolved sonar cloud issue --- .../VersionPages/TestCaseVersionPage.spec.ts | 154 ++++++++++++++ .../AppRouter/AuthenticatedAppRouter.tsx | 19 +- .../DataAssetsVersionHeader.interface.ts | 4 +- .../AddDataQualityTest/EditTestCaseModal.tsx | 4 +- .../IncidentManagerPageHeader.component.tsx | 40 +++- .../IncidentManagerPageHeader.interface.ts | 1 + .../TestCaseResultTab.component.tsx | 99 ++++++--- .../TestCaseResultTab.test.tsx | 7 + .../test-case-result-tab.style.less | 8 + .../DataQualityTab/DataQualityTab.tsx | 8 +- .../IncidentManager.component.tsx | 4 +- .../ui/src/constants/Feeds.constants.ts | 1 + .../resources/ui/src/constants/constants.ts | 8 +- .../IncidentManager.interface.ts | 2 +- .../IncidentManagerDetailPage.test.tsx | 4 +- .../IncidentManagerDetailPage.tsx | 185 +++++++++++++--- .../TestCaseClassBase.test.ts | 8 +- .../TestCaseClassBase.ts | 35 +-- .../TestCaseVersionPage.tsx | 20 ++ .../src/main/resources/ui/src/rest/testAPI.ts | 20 ++ .../src/utils/DataQuality/DataQualityUtils.ts | 4 +- .../ui/src/utils/EntityUtilClassBase.ts | 4 +- .../resources/ui/src/utils/EntityUtils.tsx | 12 +- .../src/utils/EntityVersionUtils.interface.ts | 4 +- .../ui/src/utils/EntityVersionUtils.tsx | 200 +++++++++++++++++- .../main/resources/ui/src/utils/FeedUtils.tsx | 7 +- .../resources/ui/src/utils/RouterUtils.ts | 27 ++- .../main/resources/ui/src/utils/TasksUtils.ts | 9 +- 28 files changed, 761 insertions(+), 137 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/TestCaseVersionPage.spec.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/pages/TestCaseVersionPage/TestCaseVersionPage.tsx diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/TestCaseVersionPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/TestCaseVersionPage.spec.ts new file mode 100644 index 00000000000..3d7bf3f601b --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/TestCaseVersionPage.spec.ts @@ -0,0 +1,154 @@ +/* + * 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 { expect, test } from '@playwright/test'; +import { TableClass } from '../../support/entity/TableClass'; +import { + createNewPage, + descriptionBox, + redirectToHomePage, +} from '../../utils/common'; + +// use the admin user to login +test.use({ storageState: 'playwright/.auth/admin.json' }); + +test.describe('TestCase Version Page', () => { + const table1 = new TableClass(); + + test.beforeAll('Setup pre-requests', async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + + await table1.create(apiContext); + await table1.createTestCase(apiContext); + + await afterAction(); + }); + + test.afterAll('Cleanup', async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + + await table1.delete(apiContext); + + await afterAction(); + }); + + test('should show the test case version page', async ({ page }) => { + const testCase = table1.testCasesResponseData[0]; + + await redirectToHomePage(page); + await page.goto(`/test-case/${testCase.fullyQualifiedName}`); + await page.waitForLoadState('networkidle'); + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + await test.step('Display name change', async () => { + await expect(page.getByTestId('entity-header-name')).toHaveText( + testCase.name + ); + + await expect(page.getByTestId('version-button')).toBeVisible(); + await expect(page.getByTestId('version-button')).toHaveText('0.1'); + + await page.getByTestId('manage-button').click(); + await page.getByTestId('rename-button').click(); + + await page.waitForSelector('#displayName'); + await page.fill('#displayName', 'test-case-version-changed'); + const updateNameRes = page.waitForResponse( + '/api/v1/dataQuality/testCases/*' + ); + await page.getByTestId('save-button').click(); + await updateNameRes; + + await expect(page.getByTestId('version-button')).toHaveText('0.2'); + + await page.getByTestId('version-button').click(); + await page.waitForLoadState('networkidle'); + + await expect( + page.getByTestId('entity-header-display-name').getByTestId('diff-added') + ).toHaveText('test-case-version-changed'); + + await page.getByTestId('version-button').click(); + await page.waitForLoadState('networkidle'); + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + }); + + await test.step('Description change', async () => { + await page.getByTestId('edit-description').click(); + await page.waitForSelector('[data-testid="editor"]'); + + await page.fill(descriptionBox, 'test case description changed'); + const updateDescriptionRes = page.waitForResponse( + '/api/v1/dataQuality/testCases/*' + ); + await page.getByTestId('save').click(); + await updateDescriptionRes; + + await expect(page.getByTestId('version-button')).toHaveText('0.3'); + + await page.getByTestId('version-button').click(); + await page.waitForLoadState('networkidle'); + + await expect( + page + .getByTestId('asset-description-container') + .getByTestId('markdown-parser') + .getByTestId('diff-added') + ).toHaveText('test case description changed'); + + await page.getByTestId('version-button').click(); + await page.waitForLoadState('networkidle'); + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + }); + + await test.step('Parameter change', async () => { + await page.getByTestId('edit-parameter-icon').click(); + await page.waitForSelector('#tableTestForm'); + + await page.locator('#tableTestForm_params_minValue').clear(); + await page.locator('#tableTestForm_params_minValue').fill('20'); + await page.locator('#tableTestForm_params_maxValue').clear(); + await page.locator('#tableTestForm_params_maxValue').fill('40'); + + const updateParameterRes = page.waitForResponse( + '/api/v1/dataQuality/testCases/*' + ); + await page.getByRole('button', { name: 'Submit' }).click(); + await updateParameterRes; + + await expect(page.getByTestId('version-button')).toHaveText('0.4'); + + await page.getByTestId('version-button').click(); + await page.waitForLoadState('networkidle'); + + await expect( + page.getByTestId('minValue').getByTestId('diff-removed') + ).toHaveText('12'); + await expect( + page.getByTestId('minValue').getByTestId('diff-added') + ).toHaveText('20'); + + await expect( + page.getByTestId('maxValue').getByTestId('diff-removed') + ).toHaveText('34'); + await expect( + page.getByTestId('maxValue').getByTestId('diff-added') + ).toHaveText('40'); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedAppRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedAppRouter.tsx index a434549671a..f8e7cb6e6a2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedAppRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/AppRouter/AuthenticatedAppRouter.tsx @@ -240,6 +240,12 @@ const IncidentManagerDetailPage = withSuspenseFallback( ) ); +const TestCaseVersionPage = withSuspenseFallback( + React.lazy( + () => import('../../pages/TestCaseVersionPage/TestCaseVersionPage') + ) +); + const ObservabilityAlertsPage = withSuspenseFallback( React.lazy( () => import('../../pages/ObservabilityAlertsPage/ObservabilityAlertsPage') @@ -461,9 +467,18 @@ const AuthenticatedAppRouter: FunctionComponent = () => { ResourceEntity.TEST_CASE, permissions )} + path={[ROUTES.TEST_CASE_DETAILS, ROUTES.TEST_CASE_DETAILS_WITH_TAB]} + /> + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader.interface.ts index cab9c740105..227287797af 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader.interface.ts @@ -16,6 +16,7 @@ import { APICollection } from '../../../generated/entity/data/apiCollection'; import { Database } from '../../../generated/entity/data/database'; import { DatabaseSchema } from '../../../generated/entity/data/databaseSchema'; import { EntityReference } from '../../../generated/entity/type'; +import { TestCase } from '../../../generated/tests/testCase'; import { ServicesType } from '../../../interface/service.interface'; import { VersionData } from '../../../pages/EntityVersionPage/EntityVersionPage.component'; import { TitleBreadcrumbProps } from '../../common/TitleBreadcrumb/TitleBreadcrumb.interface'; @@ -31,7 +32,8 @@ export interface DataAssetsVersionHeaderProps { | ServicesType | Database | DatabaseSchema - | APICollection; + | APICollection + | TestCase; ownerDisplayName: React.ReactNode[]; domainDisplayName?: React.ReactNode; tierDisplayName: React.ReactNode; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/EditTestCaseModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/EditTestCaseModal.tsx index 4ec2ec22f2c..1d45bc0014a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/EditTestCaseModal.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/EditTestCaseModal.tsx @@ -112,7 +112,9 @@ const EditTestCaseModal: React.FC = ({ : isEmpty(value.description) ? undefined : value.description, - displayName: value.displayName, + displayName: showOnlyParameter + ? testCase?.displayName + : value.displayName, computePassedFailedRowCount: isComputeRowCountFieldVisible ? value.computePassedFailedRowCount : testCase?.computePassedFailedRowCount, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.component.tsx index 0ad7bfc439b..25c9cf0e523 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.component.tsx @@ -25,7 +25,10 @@ import { Thread, ThreadTaskStatus, } from '../../../../generated/entity/feed/thread'; -import { EntityReference } from '../../../../generated/tests/testCase'; +import { + ChangeDescription, + EntityReference, +} from '../../../../generated/tests/testCase'; import { Severities, TestCaseResolutionStatus, @@ -55,11 +58,13 @@ import { IncidentManagerPageHeaderProps } from './IncidentManagerPageHeader.inte import { ReactComponent as InternalLinkIcon } from '../../../../assets/svg/InternalIcons.svg'; +import { getCommonExtraInfoForVersionDetails } from '../../../../utils/EntityVersionUtils'; import { getTaskDetailPath } from '../../../../utils/TasksUtils'; import './incident-manager.less'; const IncidentManagerPageHeader = ({ onOwnerUpdate, fetchTaskCount, + isVersionPage = false, }: IncidentManagerPageHeaderProps) => { const { t } = useTranslation(); const [activeTask, setActiveTask] = useState(); @@ -78,6 +83,13 @@ const IncidentManagerPageHeader = ({ updateTestCaseIncidentStatus, } = useActivityFeedProvider(); + const { ownerDisplayName, ownerRef } = useMemo(() => { + return getCommonExtraInfoForVersionDetails( + testCaseData?.changeDescription as ChangeDescription, + testCaseData?.owners + ); + }, [testCaseData?.changeDescription, testCaseData?.owners]); + const columnName = useMemo(() => { const isColumn = testCaseData?.entityLink.includes('::columns::'); if (isColumn) { @@ -161,7 +173,7 @@ const IncidentManagerPageHeader = ({ const { data } = await getListTestCaseIncidentByStateId(id); setTestCaseStatusData(first(data)); - } catch (error) { + } catch { setTestCaseStatusData(undefined); } }; @@ -212,13 +224,18 @@ const IncidentManagerPageHeader = ({ }, [testCaseData]); const { hasEditStatusPermission, hasEditOwnerPermission } = useMemo(() => { - return { - hasEditStatusPermission: - testCasePermission?.EditAll || testCasePermission?.EditStatus, - hasEditOwnerPermission: - testCasePermission?.EditAll || testCasePermission?.EditOwners, - }; - }, [testCasePermission]); + return isVersionPage + ? { + hasEditStatusPermission: false, + hasEditOwnerPermission: false, + } + : { + hasEditStatusPermission: + testCasePermission?.EditAll || testCasePermission?.EditStatus, + hasEditOwnerPermission: + testCasePermission?.EditAll || testCasePermission?.EditOwners, + }; + }, [testCasePermission, isVersionPage]); const statusDetails = useMemo(() => { if (isLoading) { @@ -310,10 +327,11 @@ const IncidentManagerPageHeader = ({ - {statusDetails} + {!isVersionPage && statusDetails} {tableFqn && ( <> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.interface.ts index eda647b61cd..44a46a0708d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.interface.ts @@ -17,4 +17,5 @@ export interface IncidentManagerPageHeaderProps { onOwnerUpdate: (owner?: EntityReference[]) => Promise; testCaseData?: TestCase; fetchTaskCount: () => void; + isVersionPage?: boolean; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx index a8cf02ab192..36eeb0c7e4b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component.tsx @@ -12,26 +12,31 @@ */ import Icon from '@ant-design/icons/lib/components/Icon'; -import { Col, Divider, Row, Space, Tooltip, Typography } from 'antd'; +import { Col, Divider, Row, Space, Typography } from 'antd'; import { AxiosError } from 'axios'; import { compare } from 'fast-json-patch'; import { isEmpty, isUndefined, startCase } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { ReactComponent as EditIcon } from '../../../../assets/svg/edit-new.svg'; -import { - DE_ACTIVE_COLOR, - ICON_DIMENSION, -} from '../../../../constants/constants'; import { CSMode } from '../../../../enums/codemirror.enum'; import { EntityType } from '../../../../enums/entity.enum'; +import { useParams } from 'react-router-dom'; import { ReactComponent as StarIcon } from '../../../../assets/svg/ic-suggestions.svg'; -import { TestCaseParameterValue } from '../../../../generated/tests/testCase'; +import { EntityField } from '../../../../constants/Feeds.constants'; +import { + ChangeDescription, + TestCaseParameterValue, +} from '../../../../generated/tests/testCase'; import { useTestCaseStore } from '../../../../pages/IncidentManager/IncidentManagerDetailPage/useTestCase.store'; import { updateTestCaseById } from '../../../../rest/testAPI'; +import { + getEntityVersionByField, + getParameterValueDiffDisplay, +} from '../../../../utils/EntityVersionUtils'; import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils'; import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1'; +import { EditIconButton } from '../../../common/IconButtons/EditIconButton'; import TestSummary from '../../../Database/Profiler/TestSummary/TestSummary'; import SchemaEditor from '../../../Database/SchemaEditor/SchemaEditor'; import EditTestCaseModal from '../../AddDataQualityTest/EditTestCaseModal'; @@ -47,17 +52,24 @@ const TestCaseResultTab = () => { showAILearningBanner, testCasePermission, } = useTestCaseStore(); + const { version } = useParams<{ version: string }>(); + const isVersionPage = !isUndefined(version); const additionalComponent = testCaseResultTabClassBase.getAdditionalComponents(); const [isParameterEdit, setIsParameterEdit] = useState(false); const { hasEditPermission, hasEditDescriptionPermission } = useMemo(() => { - return { - hasEditPermission: testCasePermission?.EditAll, - hasEditDescriptionPermission: - testCasePermission?.EditAll || testCasePermission?.EditDescription, - }; - }, [testCasePermission]); + return isVersionPage + ? { + hasEditPermission: false, + hasEditDescriptionPermission: false, + } + : { + hasEditPermission: testCasePermission?.EditAll, + hasEditDescriptionPermission: + testCasePermission?.EditAll || testCasePermission?.EditDescription, + }; + }, [testCasePermission, isVersionPage]); const { withSqlParams, withoutSqlParams } = useMemo(() => { const params = testCaseData?.parameterValues ?? []; @@ -119,7 +131,28 @@ const TestCaseResultTab = () => { [] ); + const description = useMemo(() => { + return isVersionPage + ? getEntityVersionByField( + testCaseData?.changeDescription as ChangeDescription, + EntityField.DESCRIPTION, + testCaseData?.description + ) + : testCaseData?.description; + }, [ + testCaseData?.changeDescription, + testCaseData?.description, + isVersionPage, + ]); + const testCaseParams = useMemo(() => { + if (isVersionPage) { + return getParameterValueDiffDisplay( + testCaseData?.changeDescription as ChangeDescription, + testCaseData?.parameterValues + ); + } + if (testCaseData?.useDynamicAssertion) { return (