mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-17 21:46:50 +00:00
* 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:
parent
21f3c4be3c
commit
b0ff7887ae
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
@ -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,
|
||||
]}
|
||||
/>
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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" />
|
||||
|
@ -17,4 +17,5 @@ export interface IncidentManagerPageHeaderProps {
|
||||
onOwnerUpdate: (owner?: EntityReference[]) => Promise<void>;
|
||||
testCaseData?: TestCase;
|
||||
fetchTaskCount: () => void;
|
||||
isVersionPage?: boolean;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
() => ({
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)}
|
||||
|
@ -86,6 +86,7 @@ export enum EntityField {
|
||||
MUTUALLY_EXCLUSIVE = 'mutuallyExclusive',
|
||||
EXPERTS = 'experts',
|
||||
FIELDS = 'fields',
|
||||
PARAMETER_VALUES = 'parameterValues',
|
||||
}
|
||||
|
||||
export const ANNOUNCEMENT_BG = '#FFFDF8';
|
||||
|
@ -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`,
|
||||
|
@ -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',
|
||||
|
@ -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', () =>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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,
|
||||
},
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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;
|
@ -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
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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[];
|
||||
|
@ -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];
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user