Enhance Test Case Form Functionality and UI

- Added dynamic assertion toggle and validation for test case names to prevent duplicates.
- Improved form initialization with default values and enhanced parameter handling.
- Integrated existing test case fetching based on selected tables and columns.
- Updated form submission logic to create test cases and display success/error messages.
- Enhanced overall user experience with better state management and UI adjustments.
This commit is contained in:
Shailesh Parmar 2025-07-01 22:43:56 +05:30
parent 13a62cb431
commit 8d2dde74b0
2 changed files with 252 additions and 18 deletions

View File

@ -21,7 +21,10 @@ import {
Typography,
} from 'antd';
import { useForm } from 'antd/lib/form/Form';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import cryptoRandomString from 'crypto-random-string-with-promisify-polyfill';
import { isEmpty, snakeCase } from 'lodash';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as ColumnIcon } from '../../../../assets/svg/ic-column.svg';
@ -33,10 +36,12 @@ import {
import { ENTITY_NAME_REGEX } from '../../../../constants/regex.constants';
import { SearchIndex } from '../../../../enums/search.enum';
import { TagSource } from '../../../../generated/api/domains/createDataProduct';
import { CreateTestCase } from '../../../../generated/api/tests/createTestCase';
import { Table } from '../../../../generated/entity/data/table';
import { TagLabel } from '../../../../generated/tests/testCase';
import { TagLabel, TestCase } from '../../../../generated/tests/testCase';
import {
EntityType,
TestDataType,
TestDefinition,
TestPlatform,
} from '../../../../generated/tests/testDefinition';
@ -46,15 +51,26 @@ import {
FormItemLayout,
} from '../../../../interface/FormUtils.interface';
import { TableSearchSource } from '../../../../interface/search.interface';
import testCaseClassBase from '../../../../pages/IncidentManager/IncidentManagerDetailPage/TestCaseClassBase';
import { searchQuery } from '../../../../rest/searchAPI';
import { getTableDetailsByFQN } from '../../../../rest/tableAPI';
import { getListTestDefinitions } from '../../../../rest/testAPI';
import { filterSelectOptions } from '../../../../utils/CommonUtils';
import {
createTestCase,
getListTestCaseBySearch,
getListTestDefinitions,
} from '../../../../rest/testAPI';
import {
filterSelectOptions,
replaceAllSpacialCharWith_,
} from '../../../../utils/CommonUtils';
import { getEntityName } from '../../../../utils/EntityUtils';
import { generateFormFields } from '../../../../utils/formUtils';
import { generateEntityLink } from '../../../../utils/TableUtils';
import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils';
import { AsyncSelect } from '../../../common/AsyncSelect/AsyncSelect';
import SelectionCardGroup from '../../../common/SelectionCardGroup/SelectionCardGroup';
import { SelectionOption } from '../../../common/SelectionCardGroup/SelectionCardGroup.interface';
import { TestCaseFormType } from '../AddDataQualityTest.interface';
import ParameterForm from './ParameterForm';
import './TestCaseFormV1.less';
@ -63,7 +79,7 @@ export interface TestCaseFormV1Props {
drawerProps?: DrawerProps;
className?: string;
table?: Table;
onFormSubmit?: (values: FormValues) => void;
onFormSubmit?: (testCase: TestCase) => void;
onCancel?: () => void;
initialValues?: Partial<FormValues>;
loading?: boolean;
@ -79,6 +95,9 @@ interface FormValues {
tags?: TagLabel[];
glossaryTerms?: TagLabel[];
computePassedFailedRowCount?: boolean;
useDynamicAssertion?: boolean;
params?: Record<string, string | { [key: string]: string }[]>;
parameterValues?: Array<{ name: string; value: string }>;
}
type TablesCache = Map<string, TableSearchSource>;
@ -127,27 +146,48 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
onFormSubmit,
onCancel,
loading: externalLoading = false,
}) => {
}: TestCaseFormV1Props) => {
const { t } = useTranslation();
const [form] = useForm<FormValues>();
const [loading, setLoading] = useState(false);
const [testDefinitions, setTestDefinitions] = useState<TestDefinition[]>([]);
const [selectedTestDefinition, setSelectedTestDefinition] =
useState<TestDefinition>();
const [selectedTestType, setSelectedTestType] = useState<string | undefined>(
initialValues?.testTypeId
);
const [currentColumnType, setCurrentColumnType] = useState<string>();
const [isInitialized, setIsInitialized] = useState(false);
const [selectedTableData, setSelectedTableData] = useState<Table | undefined>(
table
);
const [tablesCache, setTablesCache] = useState<TablesCache>(new Map());
const [existingTestCases, setExistingTestCases] = useState<string[]>([]);
const selectedTestLevel = Form.useWatch('testLevel', form);
const selectedTable = Form.useWatch('selectedTable', form);
const selectedColumn = Form.useWatch('selectedColumn', form);
const useDynamicAssertion = Form.useWatch('useDynamicAssertion', form);
const handleSubmit = async (values: FormValues) => {
setLoading(true);
try {
await onFormSubmit?.(values);
const testCaseObj = createTestCaseObj(values);
const createdTestCase = await createTestCase(testCaseObj);
// Pass created test case to parent
onFormSubmit?.(createdTestCase);
// Close drawer if in drawer mode
if (isDrawer) {
onCancel?.();
}
// Show success message
showSuccessToast(
t('message.entity-created-successfully', {
entity: t('label.test-case'),
})
);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setLoading(false);
}
@ -157,6 +197,21 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
onCancel?.();
};
const handleValuesChange = useCallback(
(changedValues: Partial<FormValues>) => {
if (changedValues.testTypeId) {
// Handle test type selection
setSelectedTestType(changedValues.testTypeId);
const testDef = testDefinitions.find(
(definition) =>
definition.fullyQualifiedName === changedValues.testTypeId
);
setSelectedTestDefinition(testDef);
}
},
[testDefinitions]
);
const isFormLoading = loading || externalLoading;
// Reusable action buttons component
@ -273,13 +328,42 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
}
}, []);
// Initialize form with default values
const fetchExistingTestCases = useCallback(async () => {
if (!selectedTableData && !selectedTable) {
return;
}
try {
const entityFqn = selectedTableData?.fullyQualifiedName || selectedTable;
if (!entityFqn) {
return;
}
const { data } = await getListTestCaseBySearch({
limit: PAGE_SIZE_LARGE,
entityLink: `<#E::table::${entityFqn}>`,
});
const testCaseNames = data.map((testCase) => testCase.name);
setExistingTestCases(testCaseNames);
} catch (error) {
setExistingTestCases([]);
}
}, [selectedTableData, selectedTable]);
// Initialize form on mount and update params when test definition changes
useEffect(() => {
if (!isInitialized) {
form.setFieldsValue({ testLevel: TestLevel.TABLE });
setIsInitialized(true);
// Set proper params with type handling if we have initial parameter values
if (initialValues?.parameterValues?.length && selectedTestDefinition) {
form.setFieldsValue({
params: getParamsValue(),
});
}
}, [form, isInitialized]);
}
}, [isInitialized, initialValues?.parameterValues, selectedTestDefinition]);
// Handle test level changes
useEffect(() => {
@ -297,7 +381,7 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
});
setSelectedTestDefinition(undefined);
}
}, [selectedTestLevel, fetchTestDefinitions, form]);
}, [selectedTestLevel, fetchTestDefinitions]);
// Handle table selection: use cached data or fetch if needed
useEffect(() => {
@ -314,6 +398,105 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
form.setFieldsValue({ selectedColumn: undefined });
}, [selectedTable, table, tablesCache, fetchSelectedTableData, form]);
// Fetch existing test cases when table data is available
useEffect(() => {
fetchExistingTestCases();
}, [fetchExistingTestCases]);
const getSelectedTestDefinition = useCallback(() => {
const testType = isEmpty(initialValues?.testTypeId)
? selectedTestType
: initialValues?.testTypeId;
return testDefinitions.find(
(definition) => definition.fullyQualifiedName === testType
);
}, [initialValues?.testTypeId, selectedTestType, testDefinitions]);
const getParamsValue = useCallback(() => {
return initialValues?.parameterValues?.reduce(
(acc, curr) => ({
...acc,
[curr.name || '']:
getSelectedTestDefinition()?.parameterDefinition?.[0].dataType ===
TestDataType.Array
? (JSON.parse(curr?.value || '[]') as string[]).map((val) => ({
value: val,
}))
: curr?.value,
}),
{}
);
}, [initialValues?.parameterValues, getSelectedTestDefinition]);
// Compute initial params without test definition dependency for form initialization
const getInitialParamsValue = useMemo(() => {
if (!initialValues?.parameterValues?.length) {
return undefined;
}
return initialValues.parameterValues.reduce(
(acc, curr) => ({
...acc,
[curr.name || '']: curr?.value,
}),
{}
);
}, [initialValues?.parameterValues]);
const createTestCaseObj = useCallback(
(value: FormValues): CreateTestCase => {
const selectedDefinition = getSelectedTestDefinition();
const tableName =
selectedTableData?.name || selectedTable || table?.name || '';
const columnName = selectedColumn;
const name =
value.testName?.trim() ||
`${replaceAllSpacialCharWith_(columnName ?? tableName)}_${snakeCase(
selectedTestType
)}_${cryptoRandomString({
length: 4,
type: 'alphanumeric',
})}`;
// Generate entity link based on test level
const entityFqn =
selectedTableData?.fullyQualifiedName ||
selectedTable ||
table?.fullyQualifiedName ||
'';
const isColumnLevel = selectedTestLevel === TestLevel.COLUMN;
const entityLink = generateEntityLink(
isColumnLevel ? `${entityFqn}.${columnName}` : entityFqn,
isColumnLevel
);
return {
name,
displayName: name,
computePassedFailedRowCount: value.computePassedFailedRowCount,
entityLink,
testDefinition: value.testTypeId ?? '',
description: isEmpty(value.description) ? undefined : value.description,
tags: [...(value.tags ?? []), ...(value.glossaryTerms ?? [])],
...testCaseClassBase.getCreateTestCaseObject(
value as TestCaseFormType,
selectedDefinition
),
};
},
[
getSelectedTestDefinition,
selectedTableData,
selectedTable,
table,
selectedColumn,
selectedTestType,
selectedTestLevel,
]
);
// Handle column selection and fetch test definitions with column type
useEffect(() => {
if (
@ -369,7 +552,7 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
);
const generateParamsField = useMemo(() => {
if (selectedTestDefinition?.parameterDefinition) {
if (selectedTestDefinition?.parameterDefinition && !useDynamicAssertion) {
return (
<ParameterForm
definition={selectedTestDefinition}
@ -379,12 +562,16 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
}
return null;
}, [selectedTestDefinition, selectedTableData]);
}, [selectedTestDefinition, selectedTableData, useDynamicAssertion]);
const isComputeRowCountFieldVisible = useMemo(() => {
return selectedTestDefinition?.supportsRowLevelPassedFailed ?? false;
}, [selectedTestDefinition]);
const isDynamicAssertionSupported = useMemo(() => {
return selectedTestDefinition?.supportsDynamicAssertion ?? false;
}, [selectedTestDefinition]);
const testDetailsFormFields: FieldProp[] = useMemo(
() => [
{
@ -406,6 +593,19 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
max: 256,
}),
},
{
validator: (_, value) => {
if (value && existingTestCases.includes(value)) {
return Promise.reject(
t('message.entity-already-exists', {
entity: t('label.name'),
})
);
}
return Promise.resolve();
},
},
],
props: {
'data-testid': 'test-case-name',
@ -457,7 +657,25 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
},
},
],
[initialValues?.description, initialValues?.tags, t]
[initialValues?.description, initialValues?.tags, existingTestCases, t]
);
const dynamicAssertionField: FieldProp[] = useMemo(
() => [
{
name: 'useDynamicAssertion',
label: t('label.use-dynamic-assertion'),
type: FieldTypes.SWITCH,
required: false,
props: {
'data-testid': 'use-dynamic-assertion',
className: 'use-dynamic-assertion-switch',
},
id: 'root/useDynamicAssertion',
formItemLayout: FormItemLayout.HORIZONTAL,
},
],
[t]
);
const computeRowCountField: FieldProp[] = useMemo(
@ -489,10 +707,23 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
className
)}>
<Form
data-testid="test-case-form-v1"
form={form}
initialValues={initialValues}
initialValues={{
testLevel: TestLevel.TABLE,
...testCaseClassBase.initialFormValues(),
testName: replaceAllSpacialCharWith_(initialValues?.testName ?? ''),
testTypeId: initialValues?.testTypeId,
params: getInitialParamsValue,
tags: initialValues?.tags || [],
useDynamicAssertion: initialValues?.useDynamicAssertion ?? false,
...initialValues,
}}
layout="vertical"
onFinish={handleSubmit}>
name="testCaseFormV1"
preserve={false}
onFinish={handleSubmit}
onValuesChange={handleValuesChange}>
<Form.Item
label="Select on which element your test should be performed"
name="testLevel"
@ -551,6 +782,9 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
/>
</Form.Item>
{isDynamicAssertionSupported &&
generateFormFields(dynamicAssertionField)}
{selectedTestDefinition && generateParamsField}
</Card>
@ -576,11 +810,11 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
return (
<Drawer
destroyOnClose
open
footer={drawerFooter}
placement="right"
size="large"
{...drawerProps}>
{...drawerProps}
onClose={onCancel}>
<div className="drawer-form-content">{formContent}</div>
</Drawer>
);

View File

@ -143,8 +143,8 @@ const DataQualityPage = () => {
entity: t('label.test-case'),
}),
open: isTestCaseModalOpen,
onClose: handleCloseTestCaseModal,
}}
onCancel={handleCloseTestCaseModal}
/>
</DataQualityProvider>
);