diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseFormV1.less b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseFormV1.less index e6a1b5cd2c6..6adc5a14875 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseFormV1.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseFormV1.less @@ -13,6 +13,10 @@ @import (reference) '../../../../styles/variables.less'; .test-case-form-v1 { + position: relative; + min-height: 100vh; + padding-bottom: 80px; // Space for fixed buttons + .ant-form-item { margin-bottom: @size-lg; @@ -28,7 +32,8 @@ } .select-table-card, - .test-type-card { + .test-type-card, + .test-details-card { background-color: @grey-1; border: 1px solid @grey-200; border-radius: @border-radius-sm; @@ -51,9 +56,17 @@ font-weight: @font-medium; font-size: @font-size-base; } + + .card-title { + color: @grey-800; + font-size: @size-md; + font-weight: @font-medium; + margin-bottom: @size-mlg; + line-height: 1.4; + } } - .test-type-card { + .test-details-card { margin-bottom: 0; } @@ -74,4 +87,44 @@ font-size: @font-size-base; } } + + .test-case-form-actions { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: @white; + border-top: 1px solid @grey-200; + padding: @size-lg @size-xl; + display: flex; + justify-content: flex-end; + box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); + } + + // Drawer mode adjustments + &.drawer-mode { + min-height: auto; + padding-bottom: 0; + + .test-case-form-actions { + display: none; // Hide actions in drawer mode, use drawer footer instead + } + } +} + +// Drawer specific styles +.drawer-form-content { + .test-case-form-v1 { + padding-bottom: 0; + } +} + +.drawer-footer-actions { + display: flex; + justify-content: flex-end; + width: 100%; + + .ant-space { + width: auto; + } } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseFormV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseFormV1.tsx index 98160bfeb07..5b897503acf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseFormV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/TestCaseFormV1.tsx @@ -10,29 +10,48 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Card, Drawer, DrawerProps, Form, Select, Typography } from 'antd'; +import { + Button, + Card, + Drawer, + DrawerProps, + Form, + Select, + Space, + Typography, +} from 'antd'; import { useForm } from 'antd/lib/form/Form'; import classNames from 'classnames'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { ReactComponent as ColumnIcon } from '../../../../assets/svg/ic-column.svg'; import { ReactComponent as TableIcon } from '../../../../assets/svg/ic-format-table.svg'; import { PAGE_SIZE_LARGE, PAGE_SIZE_MEDIUM, } from '../../../../constants/constants'; +import { ENTITY_NAME_REGEX } from '../../../../constants/regex.constants'; import { SearchIndex } from '../../../../enums/search.enum'; +import { TagSource } from '../../../../generated/api/domains/createDataProduct'; import { Table } from '../../../../generated/entity/data/table'; +import { TagLabel } from '../../../../generated/tests/testCase'; import { EntityType, TestDefinition, TestPlatform, } from '../../../../generated/tests/testDefinition'; +import { + FieldProp, + FieldTypes, + FormItemLayout, +} from '../../../../interface/FormUtils.interface'; import { TableSearchSource } from '../../../../interface/search.interface'; import { searchQuery } from '../../../../rest/searchAPI'; import { getTableDetailsByFQN } from '../../../../rest/tableAPI'; import { getListTestDefinitions } from '../../../../rest/testAPI'; import { filterSelectOptions } from '../../../../utils/CommonUtils'; import { getEntityName } from '../../../../utils/EntityUtils'; +import { generateFormFields } from '../../../../utils/formUtils'; import { AsyncSelect } from '../../../common/AsyncSelect/AsyncSelect'; import SelectionCardGroup from '../../../common/SelectionCardGroup/SelectionCardGroup'; import { SelectionOption } from '../../../common/SelectionCardGroup/SelectionCardGroup.interface'; @@ -45,7 +64,9 @@ export interface TestCaseFormV1Props { className?: string; table?: Table; onFormSubmit?: (values: FormValues) => void; + onCancel?: () => void; initialValues?: Partial; + loading?: boolean; } interface FormValues { @@ -53,6 +74,11 @@ interface FormValues { selectedTable?: string; selectedColumn?: string; testTypeId?: string; + testName?: string; + description?: string; + tags?: TagLabel[]; + glossaryTerms?: TagLabel[]; + computePassedFailedRowCount?: boolean; } type TablesCache = Map; @@ -92,16 +118,6 @@ const convertSearchSourceToTable = (searchSource: TableSearchSource): Table => columns: searchSource.columns || [], } as Table); -/** - * TestCaseFormV1 - An improved form component for creating test cases - * - * Features: - * - Progressive test level selection (Table/Column) - * - Smart table caching with column data - * - Dynamic test type filtering based on column data types - * - Efficient column selection without additional API calls - * - Dynamic parameter form rendering based on selected test definition - */ const TestCaseFormV1: FC = ({ className, drawerProps, @@ -109,8 +125,12 @@ const TestCaseFormV1: FC = ({ isDrawer = false, table, onFormSubmit, + onCancel, + loading: externalLoading = false, }) => { + const { t } = useTranslation(); const [form] = useForm(); + const [loading, setLoading] = useState(false); const [testDefinitions, setTestDefinitions] = useState([]); const [selectedTestDefinition, setSelectedTestDefinition] = useState(); @@ -123,12 +143,47 @@ const TestCaseFormV1: FC = ({ const selectedTestLevel = Form.useWatch('testLevel', form); const selectedTable = Form.useWatch('selectedTable', form); const selectedColumn = Form.useWatch('selectedColumn', form); - const selectedTestType = Form.useWatch('testTypeId', form); - const handleSubmit = (values: FormValues) => { - onFormSubmit?.(values); + const handleSubmit = async (values: FormValues) => { + setLoading(true); + try { + await onFormSubmit?.(values); + } finally { + setLoading(false); + } }; + const handleCancel = () => { + onCancel?.(); + }; + + const isFormLoading = loading || externalLoading; + + // Reusable action buttons component + const renderActionButtons = useMemo( + () => ( + + + + + ), + [isFormLoading, handleCancel, form, t] + ); + const fetchTables = useCallback(async (searchValue = '', page = 1) => { try { const response = await searchQuery({ @@ -326,6 +381,103 @@ const TestCaseFormV1: FC = ({ return null; }, [selectedTestDefinition, selectedTableData]); + const isComputeRowCountFieldVisible = useMemo(() => { + return selectedTestDefinition?.supportsRowLevelPassedFailed ?? false; + }, [selectedTestDefinition]); + + const testDetailsFormFields: FieldProp[] = useMemo( + () => [ + { + name: 'testName', + required: false, + label: t('label.name'), + id: 'root/testName', + type: FieldTypes.TEXT, + placeholder: t('message.enter-test-case-name'), + rules: [ + { + pattern: ENTITY_NAME_REGEX, + message: t('message.entity-name-validation'), + }, + { + max: 256, + message: t('message.entity-maximum-size', { + entity: t('label.name'), + max: 256, + }), + }, + ], + props: { + 'data-testid': 'test-case-name', + }, + }, + { + name: 'description', + required: false, + label: t('label.description'), + id: 'root/description', + type: FieldTypes.DESCRIPTION, + props: { + 'data-testid': 'description', + initialValue: initialValues?.description ?? '', + style: { + margin: 0, + }, + }, + }, + { + name: 'tags', + required: false, + label: t('label.tag-plural'), + id: 'root/tags', + type: FieldTypes.TAG_SUGGESTION, + props: { + selectProps: { + 'data-testid': 'tags-selector', + }, + }, + }, + { + name: 'glossaryTerms', + required: false, + label: t('label.glossary-term-plural'), + id: 'root/glossaryTerms', + type: FieldTypes.TAG_SUGGESTION, + props: { + selectProps: { + 'data-testid': 'glossary-terms-selector', + }, + open: false, + hasNoActionButtons: true, + isTreeSelect: true, + tagType: TagSource.Glossary, + placeholder: t('label.select-field', { + field: t('label.glossary-term-plural'), + }), + }, + }, + ], + [initialValues?.description, initialValues?.tags, t] + ); + + const computeRowCountField: FieldProp[] = useMemo( + () => [ + { + name: 'computePassedFailedRowCount', + label: t('label.compute-row-count'), + type: FieldTypes.SWITCH, + helperText: t('message.compute-row-count-helper-text'), + required: false, + props: { + 'data-testid': 'compute-passed-failed-row-count', + }, + id: 'root/computePassedFailedRowCount', + formItemLayout: FormItemLayout.HORIZONTAL, + }, + ], + [] + ); + const formContent = (
= ({ showSearch api={fetchTables} placeholder="Select one or more table at a time" - size="large" /> @@ -379,7 +530,6 @@ const TestCaseFormV1: FC = ({ loading={!selectedTableData} options={columnOptions} placeholder="Select a column" - size="large" /> )} @@ -397,26 +547,41 @@ const TestCaseFormV1: FC = ({ options={testTypeOptions} placeholder="Select a test type" popupClassName="no-wrap-option" - size="large" onChange={handleTestDefinitionChange} /> {selectedTestDefinition && generateParamsField} + + + {generateFormFields(testDetailsFormFields)} + + {isComputeRowCountFieldVisible && + generateFormFields(computeRowCountField)} + + + {!isDrawer && ( +
{renderActionButtons}
+ )}
); + const drawerFooter = ( +
{renderActionButtons}
+ ); + if (isDrawer) { return ( - {formContent} +
{formContent}
); }