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
This commit is contained in:
Shailesh Parmar 2025-05-29 17:54:06 +05:30 committed by GitHub
parent 21f3c4be3c
commit b0ff7887ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 761 additions and 137 deletions

View File

@ -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');
});
});
});

View File

@ -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]}
/>
<AdminProtectedRoute
exact
component={TestCaseVersionPage}
hasPermission={userPermissions.hasViewPermissions(
ResourceEntity.TEST_CASE,
permissions
)}
path={[
ROUTES.INCIDENT_MANAGER_DETAILS,
ROUTES.INCIDENT_MANAGER_DETAILS_WITH_TAB,
ROUTES.TEST_CASE_VERSION,
ROUTES.TEST_CASE_DETAILS_WITH_TAB_VERSION,
]}
/>

View File

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

View File

@ -112,7 +112,9 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
: isEmpty(value.description)
? undefined
: value.description,
displayName: value.displayName,
displayName: showOnlyParameter
? testCase?.displayName
: value.displayName,
computePassedFailedRowCount: isComputeRowCountFieldVisible
? value.computePassedFailedRowCount
: testCase?.computePassedFailedRowCount,

View File

@ -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<Thread>();
@ -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 = ({
<OwnerLabel
hasPermission={hasEditOwnerPermission}
isCompactView={false}
owners={testCaseData?.owners}
ownerDisplayName={ownerDisplayName}
owners={testCaseData?.owners ?? ownerRef}
onUpdate={onOwnerUpdate}
/>
{statusDetails}
{!isVersionPage && statusDetails}
{tableFqn && (
<>
<Divider className="self-center m-x-sm" type="vertical" />

View File

@ -17,4 +17,5 @@ export interface IncidentManagerPageHeaderProps {
onOwnerUpdate: (owner?: EntityReference[]) => Promise<void>;
testCaseData?: TestCase;
fetchTaskCount: () => void;
isVersionPage?: boolean;
}

View File

@ -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<boolean>(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 (
<label
@ -163,7 +196,8 @@ const TestCaseResultTab = () => {
gutter={[0, 20]}>
<Col span={24}>
<DescriptionV1
description={testCaseData?.description}
wrapInCard
description={description}
entityType={EntityType.TEST_CASE}
hasEditAccess={hasEditDescriptionPermission}
showCommentsIcon={false}
@ -179,19 +213,18 @@ const TestCaseResultTab = () => {
</Typography.Text>
{hasEditPermission &&
Boolean(
withoutSqlParams.length || testCaseData?.useDynamicAssertion
testCaseData?.parameterValues?.length ||
testCaseData?.useDynamicAssertion
) && (
<Tooltip
<EditIconButton
newLook
data-testid="edit-parameter-icon"
size="small"
title={t('label.edit-entity', {
entity: t('label.parameter'),
})}>
<Icon
component={EditIcon}
data-testid="edit-parameter-icon"
style={{ color: DE_ACTIVE_COLOR, ...ICON_DIMENSION }}
onClick={() => setIsParameterEdit(true)}
/>
</Tooltip>
})}
onClick={() => setIsParameterEdit(true)}
/>
)}
</Space>
@ -199,7 +232,7 @@ const TestCaseResultTab = () => {
</Space>
</Col>
{!isUndefined(withSqlParams) ? (
{!isUndefined(withSqlParams) && !isVersionPage ? (
<Col>
{withSqlParams.map((param) => (
<Row
@ -213,17 +246,15 @@ const TestCaseResultTab = () => {
{startCase(param.name)}
</Typography.Text>
{hasEditPermission && (
<Tooltip
<EditIconButton
newLook
data-testid="edit-sql-param-icon"
size="small"
title={t('label.edit-entity', {
entity: t('label.parameter'),
})}>
<Icon
component={EditIcon}
data-testid="edit-sql-param-icon"
style={{ color: DE_ACTIVE_COLOR, ...ICON_DIMENSION }}
onClick={() => setIsParameterEdit(true)}
/>
</Tooltip>
})}
onClick={() => setIsParameterEdit(true)}
/>
)}
</Space>
</Col>

View File

@ -76,6 +76,13 @@ const mockUseTestCaseStore = {
setIsPermissionLoading: jest.fn(),
};
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: jest.fn().mockImplementation(() => ({
version: undefined,
})),
}));
jest.mock(
'../../../../pages/IncidentManager/IncidentManagerDetailPage/useTestCase.store',
() => ({

View File

@ -23,3 +23,11 @@
background-color: @grey-5;
}
}
.version-sql-expression-container {
background-color: @grey-100;
padding: @size-sm;
border-radius: @border-radius-base;
white-space: pre-wrap;
// to add query editor look and feel
font-family: monospace;
}

View File

@ -50,7 +50,7 @@ import {
import { getEntityFQN } from '../../../../utils/FeedUtils';
import {
getEntityDetailsPath,
getIncidentManagerDetailPagePath,
getTestCaseDetailPagePath,
} from '../../../../utils/RouterUtils';
import { replacePlus } from '../../../../utils/StringsUtils';
import { showErrorToast } from '../../../../utils/ToastUtils';
@ -152,7 +152,7 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
render: (name: string, record) => {
const status = record.testCaseResult?.testCaseStatus;
const urlData = {
pathname: getIncidentManagerDetailPagePath(
pathname: getTestCaseDetailPagePath(
record.fullyQualifiedName ?? ''
),
state: { breadcrumbData },
@ -422,7 +422,7 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
return acc;
}, [] as TestCaseResolutionStatus[]);
setTestCaseStatus(data);
} catch (error) {
} catch {
// do nothing
} finally {
setIsStatusLoading(false);
@ -454,7 +454,7 @@ const DataQualityTab: React.FC<DataQualityTabProps> = ({
}, [] as TestCasePermission[]);
setTestCasePermissions(data);
} catch (error) {
} catch {
// do nothing
} finally {
setIsPermissionLoading(false);

View File

@ -66,7 +66,7 @@ import {
import { getEntityName } from '../../utils/EntityUtils';
import {
getEntityDetailsPath,
getIncidentManagerDetailPagePath,
getTestCaseDetailPagePath,
} from '../../utils/RouterUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import { AsyncSelect } from '../common/AsyncSelect/AsyncSelect';
@ -392,7 +392,7 @@ const IncidentManager = ({
className="m-0 break-all text-primary"
data-testid={`test-case-${record.testCaseReference?.name}`}
style={{ maxWidth: 280 }}
to={getIncidentManagerDetailPagePath(
to={getTestCaseDetailPagePath(
record.testCaseReference?.fullyQualifiedName ?? ''
)}>
{getEntityName(record.testCaseReference)}

View File

@ -86,6 +86,7 @@ export enum EntityField {
MUTUALLY_EXCLUSIVE = 'mutuallyExclusive',
EXPERTS = 'experts',
FIELDS = 'fields',
PARAMETER_VALUES = 'parameterValues',
}
export const ANNOUNCEMENT_BG = '#FFFDF8';

View File

@ -252,8 +252,12 @@ export const ROUTES = {
DATA_QUALITY_WITH_TAB: `/data-quality/${PLACEHOLDER_ROUTE_TAB}`,
INCIDENT_MANAGER: '/incident-manager',
INCIDENT_MANAGER_DETAILS: `/incident-manager/${PLACEHOLDER_ROUTE_FQN}`,
INCIDENT_MANAGER_DETAILS_WITH_TAB: `/incident-manager/${PLACEHOLDER_ROUTE_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
// test case
TEST_CASE_DETAILS: `/test-case/${PLACEHOLDER_ROUTE_FQN}`,
TEST_CASE_DETAILS_WITH_TAB: `/test-case/${PLACEHOLDER_ROUTE_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
TEST_CASE_VERSION: `/test-case/${PLACEHOLDER_ROUTE_FQN}/versions/${PLACEHOLDER_ROUTE_VERSION}`,
TEST_CASE_DETAILS_WITH_TAB_VERSION: `/test-case/${PLACEHOLDER_ROUTE_FQN}/versions/${PLACEHOLDER_ROUTE_VERSION}/${PLACEHOLDER_ROUTE_TAB}`,
// logs viewer
LOGS: `/${LOG_ENTITY_TYPE}/${PLACEHOLDER_ROUTE_FQN}/logs`,

View File

@ -13,7 +13,7 @@
import { TestCaseResolutionStatus } from '../../generated/tests/testCaseResolutionStatus';
export enum IncidentManagerTabs {
export enum TestCasePageTabs {
TEST_CASE_RESULTS = 'test-case-results',
SQL_QUERY = 'sql-query',
ISSUES = 'issues',

View File

@ -17,7 +17,7 @@ import { TestCase } from '../../../generated/tests/testCase';
import { MOCK_PERMISSIONS } from '../../../mocks/Glossary.mock';
import { getTestCaseByFqn } from '../../../rest/testAPI';
import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils';
import { IncidentManagerTabs } from '../IncidentManager.interface';
import { TestCasePageTabs } from '../IncidentManager.interface';
import IncidentManagerDetailPage from './IncidentManagerDetailPage';
import { UseTestCaseStoreInterface } from './useTestCase.store';
@ -110,7 +110,7 @@ jest.mock('react-router-dom', () => ({
useHistory: () => mockHistory,
useParams: () => ({
fqn: 'sample_data.ecommerce_db.shopify.dim_address.table_column_count_equals',
tab: IncidentManagerTabs.TEST_CASE_RESULTS,
tab: TestCasePageTabs.TEST_CASE_RESULTS,
}),
}));
jest.mock('../../../components/PageLayoutV1/PageLayoutV1', () =>

View File

@ -10,14 +10,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Col, Row, Tabs, TabsProps } from 'antd';
import Icon from '@ant-design/icons';
import { Button, Col, Row, Tabs, TabsProps, Tooltip, Typography } from 'antd';
import ButtonGroup from 'antd/lib/button/button-group';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { compare, Operation as PatchOperation } from 'fast-json-patch';
import { isUndefined } from 'lodash';
import { isUndefined, toString } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import { ReactComponent as TestCaseIcon } from '../../../assets/svg/ic-checklist.svg';
import { ReactComponent as VersionIcon } from '../../../assets/svg/ic-version.svg';
import { withActivityFeed } from '../../../components/AppRouter/withActivityFeed';
import ManageButton from '../../../components/common/EntityPageInfos/ManageButton/ManageButton';
import ErrorPlaceHolder from '../../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
@ -26,35 +30,54 @@ import TitleBreadcrumb from '../../../components/common/TitleBreadcrumb/TitleBre
import { TitleBreadcrumbProps } from '../../../components/common/TitleBreadcrumb/TitleBreadcrumb.interface';
import IncidentManagerPageHeader from '../../../components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.component';
import EntityHeaderTitle from '../../../components/Entity/EntityHeaderTitle/EntityHeaderTitle.component';
import EntityVersionTimeLine from '../../../components/Entity/EntityVersionTimeLine/EntityVersionTimeLine';
import { EntityName } from '../../../components/Modals/EntityNameModal/EntityNameModal.interface';
import PageLayoutV1 from '../../../components/PageLayoutV1/PageLayoutV1';
import { ROUTES } from '../../../constants/constants';
import { FEED_COUNT_INITIAL_DATA } from '../../../constants/entity.constants';
import { EntityField } from '../../../constants/Feeds.constants';
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../../../context/PermissionProvider/PermissionProvider.interface';
import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum';
import { EntityTabs, EntityType } from '../../../enums/entity.enum';
import { EntityReference } from '../../../generated/tests/testCase';
import {
ChangeDescription,
EntityReference,
} from '../../../generated/tests/testCase';
import { EntityHistory } from '../../../generated/type/entityHistory';
import { useFqn } from '../../../hooks/useFqn';
import { FeedCounts } from '../../../interface/feed.interface';
import { getTestCaseByFqn, updateTestCaseById } from '../../../rest/testAPI';
import {
getTestCaseByFqn,
getTestCaseVersionDetails,
getTestCaseVersionList,
updateTestCaseById,
} from '../../../rest/testAPI';
import { getFeedCounts } from '../../../utils/CommonUtils';
import { getEntityName } from '../../../utils/EntityUtils';
import { getIncidentManagerDetailPagePath } from '../../../utils/RouterUtils';
import { getEntityVersionByField } from '../../../utils/EntityVersionUtils';
import {
getTestCaseDetailPagePath,
getTestCaseVersionPath,
} from '../../../utils/RouterUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import { IncidentManagerTabs } from '../IncidentManager.interface';
import { TestCasePageTabs } from '../IncidentManager.interface';
import './incident-manager-details.less';
import testCaseClassBase from './TestCaseClassBase';
import { useTestCaseStore } from './useTestCase.store';
const IncidentManagerDetailPage = () => {
const IncidentManagerDetailPage = ({
isVersionPage = false,
}: {
isVersionPage?: boolean;
}) => {
const { t } = useTranslation();
const history = useHistory();
const location =
useLocation<{ breadcrumbData: TitleBreadcrumbProps['titleLinks'] }>();
const { tab: activeTab = IncidentManagerTabs.TEST_CASE_RESULTS } =
useParams<{ tab: EntityTabs }>();
const { tab: activeTab = TestCasePageTabs.TEST_CASE_RESULTS, version } =
useParams<{ tab: EntityTabs; version: string }>();
const { fqn: testCaseFQN } = useFqn();
@ -72,6 +95,10 @@ const IncidentManagerDetailPage = () => {
const [feedCount, setFeedCount] = useState<FeedCounts>(
FEED_COUNT_INITIAL_DATA
);
const [versionList, setVersionList] = useState<EntityHistory>({
entityType: EntityType.TEST_CASE,
versions: [],
});
const { getEntityPermissionByFqn } = usePermissionProvider();
const { hasViewPermission, editDisplayNamePermission, hasDeletePermission } =
@ -86,7 +113,10 @@ const IncidentManagerDetailPage = () => {
}, [testCasePermission]);
const tabDetails: TabsProps['items'] = useMemo(() => {
const tabs = testCaseClassBase.getTab(feedCount.openTaskCount);
const tabs = testCaseClassBase.getTab(
feedCount.openTaskCount,
isVersionPage
);
return tabs.map(({ LabelComponent, labelProps, key, Tab }) => ({
key,
@ -120,6 +150,10 @@ const IncidentManagerDetailPage = () => {
testCaseClassBase.setShowSqlQueryTab(
!isUndefined(response.inspectionQuery)
);
if (isVersionPage) {
const versionResponse = await getTestCaseVersionList(response.id ?? '');
setVersionList(versionResponse);
}
setTestCase(response);
} catch (error) {
showErrorToast(
@ -155,10 +189,16 @@ const IncidentManagerDetailPage = () => {
const handleTabChange = (activeKey: string) => {
if (activeKey !== activeTab) {
history.push(
getIncidentManagerDetailPagePath(
testCaseFQN,
activeKey as IncidentManagerTabs
)
isVersionPage
? getTestCaseVersionPath(
testCaseFQN,
version,
activeKey as TestCasePageTabs
)
: getTestCaseDetailPagePath(
testCaseFQN,
activeKey as TestCasePageTabs
)
);
}
};
@ -210,6 +250,46 @@ const IncidentManagerDetailPage = () => {
getFeedCounts(EntityType.TEST_CASE, testCaseFQN, handleFeedCount);
}, [testCaseFQN]);
const onVersionClick = () => {
history.push(
isVersionPage
? getTestCaseDetailPagePath(testCaseFQN)
: getTestCaseVersionPath(
testCaseFQN,
toString(testCase?.version) ?? '',
activeTab
)
);
};
// version related methods
const versionHandler = useCallback(
(newVersion = version) => {
history.push(
getTestCaseVersionPath(testCaseFQN, toString(newVersion), activeTab)
);
},
[testCaseFQN, activeTab]
);
const fetchCurrentVersion = async (id: string) => {
try {
const response = await getTestCaseVersionDetails(id, version);
setTestCase(response);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const displayName = useMemo(() => {
return isVersionPage
? getEntityVersionByField(
testCase?.changeDescription as ChangeDescription,
EntityField.DISPLAYNAME,
testCase?.displayName
)
: testCase?.displayName;
}, [testCase?.changeDescription, testCase?.displayName, isVersionPage]);
useEffect(() => {
if (testCaseFQN) {
fetchTestCasePermission();
@ -231,6 +311,12 @@ const IncidentManagerDetailPage = () => {
};
}, [testCaseFQN, hasViewPermission]);
useEffect(() => {
if (testCase?.id && isVersionPage) {
fetchCurrentVersion(testCase.id);
}
}, [version, testCase?.id, isVersionPage]);
if (isLoading || isPermissionLoading) {
return <Loader />;
}
@ -253,10 +339,18 @@ const IncidentManagerDetailPage = () => {
return (
<PageLayoutV1
pageTitle={t('label.entity-detail-plural', {
entity: getEntityName(testCase) || t('label.test-case'),
})}>
pageTitle={t(
isVersionPage
? 'label.entity-version-detail-plural'
: 'label.entity-detail-plural',
{
entity: getEntityName(testCase) || t('label.test-case'),
}
)}>
<Row
className={classNames({
'version-data': isVersionPage,
})}
data-testid="incident-manager-details-page-container"
gutter={[0, 12]}>
<Col span={24}>
@ -267,32 +361,52 @@ const IncidentManagerDetailPage = () => {
<Col span={23}>
<EntityHeaderTitle
className="w-max-full-45"
displayName={testCase?.displayName}
displayName={displayName}
icon={<TestCaseIcon className="h-9" />}
name={testCase?.name ?? ''}
serviceName="testCase"
/>
</Col>
<Col className="d-flex justify-end" span={1}>
<ManageButton
isRecursiveDelete
afterDeleteAction={() => history.push(ROUTES.INCIDENT_MANAGER)}
allowSoftDelete={false}
canDelete={hasDeletePermission}
displayName={testCase.displayName}
editDisplayNamePermission={editDisplayNamePermission}
entityFQN={testCase.fullyQualifiedName}
entityId={testCase.id}
entityName={testCase.name}
entityType={EntityType.TEST_CASE}
onEditDisplayName={handleDisplayNameChange}
/>
<ButtonGroup
className="data-asset-button-group spaced"
data-testid="asset-header-btn-group"
size="small">
<Tooltip title={t('label.version-plural-history')}>
<Button
className="version-button"
data-testid="version-button"
icon={<Icon component={VersionIcon} />}
onClick={onVersionClick}>
<Typography.Text>{testCase?.version}</Typography.Text>
</Button>
</Tooltip>
{!isVersionPage && (
<ManageButton
isRecursiveDelete
afterDeleteAction={() =>
history.push(ROUTES.INCIDENT_MANAGER)
}
allowSoftDelete={false}
canDelete={hasDeletePermission}
displayName={testCase.displayName}
editDisplayNamePermission={editDisplayNamePermission}
entityFQN={testCase.fullyQualifiedName}
entityId={testCase.id}
entityName={testCase.name}
entityType={EntityType.TEST_CASE}
onEditDisplayName={handleDisplayNameChange}
/>
)}
</ButtonGroup>
</Col>
</Row>
</Col>
<Col className="w-full">
<IncidentManagerPageHeader
fetchTaskCount={getEntityFeedCount}
isVersionPage={isVersionPage}
testCaseData={testCase}
onOwnerUpdate={handleOwnerChange}
/>
@ -308,6 +422,15 @@ const IncidentManagerDetailPage = () => {
/>
</Col>
</Row>
{isVersionPage && (
<EntityVersionTimeLine
currentVersion={toString(version)}
entityType={EntityType.TEST_CASE}
versionHandler={versionHandler}
versionList={versionList}
onBack={onVersionClick}
/>
)}
</PageLayoutV1>
);
};

View File

@ -18,7 +18,7 @@ import { CreateTestCase } from '../../../generated/api/tests/createTestCase';
import { TestDefinition } from '../../../generated/tests/testDefinition';
import { createTestCaseParameters } from '../../../utils/DataQuality/DataQualityUtils';
import i18n from '../../../utils/i18next/LocalUtil';
import { IncidentManagerTabs } from '../IncidentManager.interface';
import { TestCasePageTabs } from '../IncidentManager.interface';
import { TestCaseClassBase, TestCaseTabType } from './TestCaseClassBase';
describe('TestCaseClassBase', () => {
@ -48,7 +48,7 @@ describe('TestCaseClassBase', () => {
name: i18n.t('label.test-case-result'),
},
Tab: TestCaseResultTab,
key: IncidentManagerTabs.TEST_CASE_RESULTS,
key: TestCasePageTabs.TEST_CASE_RESULTS,
},
{
LabelComponent: TabsLabel,
@ -58,11 +58,11 @@ describe('TestCaseClassBase', () => {
count: openTaskCount,
},
Tab: TestCaseIncidentTab,
key: IncidentManagerTabs.ISSUES,
key: TestCasePageTabs.ISSUES,
},
];
const result = testCaseClassBase.getTab(openTaskCount);
const result = testCaseClassBase.getTab(openTaskCount, false);
expect(result).toEqual(expectedTabs);
});

View File

@ -22,13 +22,13 @@ import { TestDefinition } from '../../../generated/tests/testDefinition';
import { FieldProp } from '../../../interface/FormUtils.interface';
import { createTestCaseParameters } from '../../../utils/DataQuality/DataQualityUtils';
import i18n from '../../../utils/i18next/LocalUtil';
import { IncidentManagerTabs } from '../IncidentManager.interface';
import { TestCasePageTabs } from '../IncidentManager.interface';
export interface TestCaseTabType {
LabelComponent: typeof TabsLabel;
labelProps: TabsLabelProps;
Tab: () => ReactElement;
key: IncidentManagerTabs;
key: TestCasePageTabs;
}
class TestCaseClassBase {
@ -38,7 +38,10 @@ class TestCaseClassBase {
this.showSqlQueryTab = false;
}
public getTab(openTaskCount: number): TestCaseTabType[] {
public getTab(
openTaskCount: number,
isVersionPage: boolean
): TestCaseTabType[] {
return [
{
LabelComponent: TabsLabel,
@ -47,18 +50,22 @@ class TestCaseClassBase {
name: i18n.t('label.test-case-result'),
},
Tab: TestCaseResultTab,
key: IncidentManagerTabs.TEST_CASE_RESULTS,
},
{
LabelComponent: TabsLabel,
labelProps: {
id: 'incident',
name: i18n.t('label.incident'),
count: openTaskCount,
},
Tab: TestCaseIncidentTab,
key: IncidentManagerTabs.ISSUES,
key: TestCasePageTabs.TEST_CASE_RESULTS,
},
...(isVersionPage
? []
: [
{
LabelComponent: TabsLabel,
labelProps: {
id: 'incident',
name: i18n.t('label.incident'),
count: openTaskCount,
},
Tab: TestCaseIncidentTab,
key: TestCasePageTabs.ISSUES,
},
]),
];
}

View File

@ -0,0 +1,20 @@
/*
* 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 React from 'react';
import IncidentManagerDetailPage from '../IncidentManager/IncidentManagerDetailPage/IncidentManagerDetailPage';
const TestCaseVersionPage = () => {
return <IncidentManagerDetailPage isVersionPage />;
};
export default TestCaseVersionPage;

View File

@ -30,6 +30,7 @@ import {
TestPlatform,
} from '../generated/tests/testDefinition';
import { TestSuite, TestSummary } from '../generated/tests/testSuite';
import { EntityHistory } from '../generated/type/entityHistory';
import { Paging } from '../generated/type/paging';
import { ListParams } from '../interface/API.interface';
import { getEncodedFqn } from '../utils/StringsUtils';
@ -217,6 +218,25 @@ export const removeTestCaseFromTestSuite = async (
return response.data;
};
export const getTestCaseVersionList = async (id: string) => {
const url = `${testCaseUrl}/${id}/versions`;
const response = await APIClient.get<EntityHistory>(url);
return response.data;
};
export const getTestCaseVersionDetails = async (
id: string,
version: string
) => {
const url = `${testCaseUrl}/${id}/versions/${version}`;
const response = await APIClient.get(url);
return response.data;
};
// testDefinition Section
export const getListTestDefinitions = async (
params?: ListTestDefinitionsParams

View File

@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { cloneDeep, isArray, isUndefined, omit, omitBy } from 'lodash';
import { cloneDeep, isArray, isNil, isUndefined, omit, omitBy } from 'lodash';
import { ReactComponent as AccuracyIcon } from '../../assets/svg/ic-accuracy.svg';
import { ReactComponent as CompletenessIcon } from '../../assets/svg/ic-completeness.svg';
import { ReactComponent as ConsistencyIcon } from '../../assets/svg/ic-consistency.svg';
@ -84,7 +84,7 @@ export const createTestCaseParameters = (
if (arrayValues.length) {
acc.push({ name: key, value: JSON.stringify(arrayValues) });
}
} else {
} else if (!isNil(value)) {
acc.push({ name: key, value: value as string });
}

View File

@ -57,7 +57,6 @@ import {
getEditWebhookPath,
getEntityDetailsPath,
getGlossaryTermDetailsPath,
getIncidentManagerDetailPagePath,
getNotificationAlertDetailsPath,
getObservabilityAlertDetailsPath,
getPersonaDetailsPath,
@ -67,6 +66,7 @@ import {
getSettingPath,
getTagsDetailsPath,
getTeamsWithFqnPath,
getTestCaseDetailPagePath,
getUserPath,
} from './RouterUtils';
import { ExtraTableDropdownOptions } from './TableUtils';
@ -192,7 +192,7 @@ class EntityUtilClassBase {
);
case EntityType.TEST_CASE:
return getIncidentManagerDetailPagePath(fullyQualifiedName);
return getTestCaseDetailPagePath(fullyQualifiedName);
case EntityType.TEST_SUITE:
return getTestSuiteDetailsPath({

View File

@ -127,7 +127,6 @@ import {
getEntityDetailsPath,
getGlossaryPath,
getGlossaryTermDetailsPath,
getIncidentManagerDetailPagePath,
getKpiPath,
getNotificationAlertDetailsPath,
getObservabilityAlertDetailsPath,
@ -138,6 +137,7 @@ import {
getSettingPath,
getTagsDetailsPath,
getTeamsWithFqnPath,
getTestCaseDetailPagePath,
} from './RouterUtils';
import { getServiceRouteFromServiceType } from './ServiceUtils';
import { bytesToSize, getEncodedFqn, stringToHTML } from './StringsUtils';
@ -188,8 +188,8 @@ export const getEntityTags = (
switch (type) {
case EntityType.TABLE: {
const tableTags: Array<TagLabel> = [
...getTableTags((entityDetail as Table).columns || []),
...(entityDetail.tags || []),
...getTableTags((entityDetail as Table).columns ?? []),
...(entityDetail.tags ?? []),
];
return tableTags;
@ -197,13 +197,13 @@ export const getEntityTags = (
case EntityType.DASHBOARD:
case EntityType.SEARCH_INDEX:
case EntityType.PIPELINE:
return getTagsWithoutTier(entityDetail.tags || []);
return getTagsWithoutTier(entityDetail.tags ?? []);
case EntityType.TOPIC:
case EntityType.MLMODEL:
case EntityType.STORED_PROCEDURE:
case EntityType.DASHBOARD_DATA_MODEL: {
return entityDetail.tags || [];
return entityDetail.tags ?? [];
}
default:
@ -1581,7 +1581,7 @@ export const getEntityLinkFromType = (
case EntityType.APPLICATION:
return getApplicationDetailsPath(fullyQualifiedName);
case EntityType.TEST_CASE:
return getIncidentManagerDetailPagePath(fullyQualifiedName);
return getTestCaseDetailPagePath(fullyQualifiedName);
case EntityType.TEST_SUITE:
return getEntityDetailsPath(
EntityType.TABLE,

View File

@ -19,6 +19,7 @@ import { Glossary } from '../generated/entity/data/glossary';
import { GlossaryTerm } from '../generated/entity/data/glossaryTerm';
import { Column as TableColumn } from '../generated/entity/data/table';
import { Field } from '../generated/entity/data/topic';
import { TestCase } from '../generated/tests/testCase';
import { TagLabel } from '../generated/type/tagLabel';
import { ServicesType } from '../interface/service.interface';
import { VersionData } from '../pages/EntityVersionPage/EntityVersionPage.component';
@ -40,6 +41,7 @@ export type VersionEntityTypes =
| ServicesType
| Database
| DatabaseSchema
| APICollection;
| APICollection
| TestCase;
export type AssetsChildForVersionPages = TableColumn | ContainerColumn | Field;

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { Typography } from 'antd';
import { Divider, Space, Typography } from 'antd';
import {
ArrayChange,
Change,
@ -27,6 +27,7 @@ import {
isEqual,
isObject,
isUndefined,
startCase,
toString,
uniqBy,
uniqueId,
@ -54,6 +55,7 @@ import {
} from '../generated/entity/services/databaseService';
import { MetadataService } from '../generated/entity/services/metadataService';
import { EntityReference } from '../generated/entity/type';
import { TestCaseParameterValue } from '../generated/tests/testCase';
import { TagLabel } from '../generated/type/tagLabel';
import {
EntityDiffProps,
@ -631,8 +633,8 @@ export const getCommonExtraInfoForVersionDetails = (
if (!isUndefined(newTier) || !isUndefined(oldTier)) {
tierDisplayName = getDiffValue(
oldTier?.tagFQN?.split(FQN_SEPARATOR_CHAR)[1] || '',
newTier?.tagFQN?.split(FQN_SEPARATOR_CHAR)[1] || ''
oldTier?.tagFQN?.split(FQN_SEPARATOR_CHAR)[1] ?? '',
newTier?.tagFQN?.split(FQN_SEPARATOR_CHAR)[1] ?? ''
);
} else if (tier?.tagFQN) {
tierDisplayName = tier?.tagFQN.split(FQN_SEPARATOR_CHAR)[1];
@ -1032,6 +1034,198 @@ export const getOwnerDiff = (
};
};
export const getChangedEntityStatus = (
oldValue?: string,
newValue?: string
) => {
if (oldValue && newValue) {
return oldValue !== newValue
? EntityChangeOperations.UPDATED
: EntityChangeOperations.NORMAL;
} else if (oldValue && !newValue) {
return EntityChangeOperations.DELETED;
} else if (!oldValue && newValue) {
return EntityChangeOperations.ADDED;
}
return EntityChangeOperations.NORMAL;
};
export const getParameterValuesDiff = (
changeDescription: ChangeDescription,
defaultValues?: TestCaseParameterValue[]
): {
name: string;
oldValue: string;
newValue: string;
status: EntityChangeOperations;
}[] => {
const fieldDiff = getDiffByFieldName(
EntityField.PARAMETER_VALUES,
changeDescription,
true
);
const oldValues: TestCaseParameterValue[] =
getChangedEntityOldValue(fieldDiff) ?? [];
const newValues: TestCaseParameterValue[] =
getChangedEntityNewValue(fieldDiff) ?? [];
// If no diffs exist and we have default values, return them as unchanged
if (
isEmpty(oldValues) &&
isEmpty(newValues) &&
defaultValues &&
!isEmpty(defaultValues)
) {
return defaultValues.map((param) => ({
name: String(param.name),
oldValue: String(param.value),
newValue: String(param.value),
status: EntityChangeOperations.NORMAL,
}));
}
const result: {
name: string;
oldValue: string;
newValue: string;
status: EntityChangeOperations;
}[] = [];
// Find all unique parameter names
const allNames = Array.from(
new Set([...oldValues.map((p) => p.name), ...newValues.map((p) => p.name)])
);
allNames.forEach((name) => {
const oldParam = oldValues.find((p) => p.name === name);
const newParam = newValues.find((p) => p.name === name);
if (oldParam && newParam) {
if (oldParam.value !== newParam.value) {
result.push({
name: String(name),
oldValue: String(oldParam.value),
newValue: String(newParam.value),
status: EntityChangeOperations.UPDATED,
});
} else {
result.push({
name: String(name),
oldValue: String(oldParam.value),
newValue: String(newParam.value),
status: EntityChangeOperations.NORMAL,
});
}
} else if (!oldParam && newParam) {
result.push({
name: String(name),
oldValue: '',
newValue: String(newParam.value),
status: EntityChangeOperations.ADDED,
});
} else if (oldParam && !newParam) {
result.push({
name: String(name),
oldValue: String(oldParam.value),
newValue: '',
status: EntityChangeOperations.DELETED,
});
}
});
return result;
};
// New function for React element diff (for parameter value diff only)
export const getTextDiffElements = (
oldText: string,
newText: string
): React.ReactNode[] => {
const diffArr = diffWords(toString(oldText), toString(newText));
return diffArr.map((diff) => {
if (diff.added) {
return getAddedDiffElement(diff.value);
}
if (diff.removed) {
return getRemovedDiffElement(diff.value);
}
return getNormalDiffElement(diff.value);
});
};
export const getDiffDisplayValue = (diff: {
oldValue: string;
newValue: string;
status: EntityChangeOperations;
}) => {
switch (diff.status) {
case EntityChangeOperations.UPDATED:
return getTextDiffElements(diff.oldValue, diff.newValue);
case EntityChangeOperations.ADDED:
return getAddedDiffElement(diff.newValue);
case EntityChangeOperations.DELETED:
return getRemovedDiffElement(diff.oldValue);
case EntityChangeOperations.NORMAL:
default:
return diff.oldValue;
}
};
export const getParameterValueDiffDisplay = (
changeDescription: ChangeDescription,
defaultValues?: TestCaseParameterValue[]
): React.ReactNode => {
const diffs = getParameterValuesDiff(changeDescription, defaultValues);
// Separate sqlExpression from other params
const sqlParamDiff = diffs.find((diff) => diff.name === 'sqlExpression');
const otherParamDiffs = diffs.filter((diff) => diff.name !== 'sqlExpression');
return (
<>
{/* Render non-sqlExpression parameters as before */}
<Space
wrap
className="parameter-value-container parameter-value"
size={6}>
{otherParamDiffs.length === 0 ? (
<Typography.Text type="secondary">
{t('label.no-parameter-available')}
</Typography.Text>
) : (
otherParamDiffs.map((diff, index) => (
<Space data-testid={diff.name} key={diff.name} size={4}>
<Typography.Text className="text-grey-muted">
{`${diff.name}:`}
</Typography.Text>
<Typography.Text>{getDiffDisplayValue(diff)}</Typography.Text>
{otherParamDiffs.length - 1 !== index && (
<Divider type="vertical" />
)}
</Space>
))
)}
</Space>
{/* Render sqlExpression parameter separately, using inline diff in a code-style block */}
{sqlParamDiff && (
<div className="m-t-md">
<Typography.Text className="right-panel-label">
{startCase(sqlParamDiff.name)}
</Typography.Text>
<div className="m-t-sm version-sql-expression-container">
{getDiffDisplayValue(sqlParamDiff)}
</div>
</div>
)}
</>
);
};
export const getOwnerVersionLabel = (
entity: {
[TabSpecificField.OWNERS]?: EntityReference[];

View File

@ -133,13 +133,11 @@ export const getReplyText = (
return i18next.t('label.reply-in-conversation');
}
if (count === 1) {
return `${count} ${
singular ? singular : i18next.t('label.older-reply-lowercase')
}`;
return `${count} ${singular ?? i18next.t('label.older-reply-lowercase')}`;
}
return `${count} ${
plural ? plural : i18next.t('label.older-reply-plural-lowercase')
plural ?? i18next.t('label.older-reply-plural-lowercase')
}`;
};
@ -629,6 +627,7 @@ export const getFeedChangeFieldLabel = (fieldName?: EntityField) => {
[EntityField.MUTUALLY_EXCLUSIVE]: i18next.t('label.mutually-exclusive'),
[EntityField.EXPERTS]: i18next.t('label.expert-plural'),
[EntityField.FIELDS]: i18next.t('label.field-plural'),
[EntityField.PARAMETER_VALUES]: i18next.t('label.parameter-plural'),
};
return isUndefined(fieldName) ? '' : fieldNameLabelMapping[fieldName];

View File

@ -47,7 +47,7 @@ import { ServiceAgentSubTabs } from '../enums/service.enum';
import { ProfilerDashboardType } from '../enums/table.enum';
import { PipelineType } from '../generated/api/services/ingestionPipelines/createIngestionPipeline';
import { DataQualityPageTabs } from '../pages/DataQuality/DataQualityPage.interface';
import { IncidentManagerTabs } from '../pages/IncidentManager/IncidentManager.interface';
import { TestCasePageTabs } from '../pages/IncidentManager/IncidentManager.interface';
import { getPartialNameFromFQN } from './CommonUtils';
import { getBasePath } from './HistoryUtils';
import { getServiceRouteFromServiceType } from './ServiceUtils';
@ -497,11 +497,11 @@ export const getDataQualityPagePath = (tab?: DataQualityPageTabs) => {
return path;
};
export const getIncidentManagerDetailPagePath = (
export const getTestCaseDetailPagePath = (
fqn: string,
tab = IncidentManagerTabs.TEST_CASE_RESULTS
tab = TestCasePageTabs.TEST_CASE_RESULTS
) => {
let path = ROUTES.INCIDENT_MANAGER_DETAILS_WITH_TAB;
let path = ROUTES.TEST_CASE_DETAILS_WITH_TAB;
path = path
.replace(PLACEHOLDER_ROUTE_FQN, getEncodedFqn(fqn))
@ -509,6 +509,25 @@ export const getIncidentManagerDetailPagePath = (
return path;
};
export const getTestCaseVersionPath = (
fqn: string,
version: string,
tab?: string
) => {
let path = tab
? ROUTES.TEST_CASE_DETAILS_WITH_TAB_VERSION
: ROUTES.TEST_CASE_VERSION;
path = path
.replace(PLACEHOLDER_ROUTE_FQN, getEncodedFqn(fqn))
.replace(PLACEHOLDER_ROUTE_VERSION, version);
if (tab) {
path = path.replace(PLACEHOLDER_ROUTE_TAB, tab);
}
return path;
};
export const getServiceVersionPath = (
serviceCategory: string,

View File

@ -50,7 +50,7 @@ import { TaskType, Thread } from '../generated/entity/feed/thread';
import { EntityReference } from '../generated/entity/type';
import { TagLabel } from '../generated/type/tagLabel';
import { SearchSourceAlias } from '../interface/search.interface';
import { IncidentManagerTabs } from '../pages/IncidentManager/IncidentManager.interface';
import { TestCasePageTabs } from '../pages/IncidentManager/IncidentManager.interface';
import {
EntityData,
Option,
@ -94,8 +94,8 @@ import { defaultFields as PipelineFields } from './PipelineDetailsUtils';
import {
getEntityDetailsPath,
getGlossaryTermDetailsPath,
getIncidentManagerDetailPagePath,
getServiceDetailsPath,
getTestCaseDetailPagePath,
getUserPath,
} from './RouterUtils';
import serviceUtilClassBase from './ServiceUtilClassBase';
@ -188,10 +188,7 @@ export const getTaskDetailPath = (task: Thread) => {
const entityType = getEntityType(task.about) ?? '';
if (entityType === EntityType.TEST_CASE) {
return getIncidentManagerDetailPagePath(
entityFqn,
IncidentManagerTabs.ISSUES
);
return getTestCaseDetailPagePath(entityFqn, TestCasePageTabs.ISSUES);
} else if (entityType === EntityType.USER) {
return getUserPath(
entityFqn,