mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-07 08:03:09 +00:00
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:
parent
13a62cb431
commit
8d2dde74b0
@ -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>
|
||||
);
|
||||
|
@ -143,8 +143,8 @@ const DataQualityPage = () => {
|
||||
entity: t('label.test-case'),
|
||||
}),
|
||||
open: isTestCaseModalOpen,
|
||||
onClose: handleCloseTestCaseModal,
|
||||
}}
|
||||
onCancel={handleCloseTestCaseModal}
|
||||
/>
|
||||
</DataQualityProvider>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user