mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-03 14:13:06 +00:00
Enhance Test Case Form with New Features and Improved UI
- Added new fields for test name, description, tags, and glossary terms to the Test Case Form. - Implemented loading state management for form submission and cancellation. - Introduced reusable action buttons for better user interaction. - Enhanced styling for fixed action buttons and drawer mode adjustments. - Updated form layout to accommodate new fields and improve overall user experience.
This commit is contained in:
parent
2aa5431488
commit
13a62cb431
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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<FormValues>;
|
||||
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<string, TableSearchSource>;
|
||||
@ -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<TestCaseFormV1Props> = ({
|
||||
className,
|
||||
drawerProps,
|
||||
@ -109,8 +125,12 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
||||
isDrawer = false,
|
||||
table,
|
||||
onFormSubmit,
|
||||
onCancel,
|
||||
loading: externalLoading = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = useForm<FormValues>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [testDefinitions, setTestDefinitions] = useState<TestDefinition[]>([]);
|
||||
const [selectedTestDefinition, setSelectedTestDefinition] =
|
||||
useState<TestDefinition>();
|
||||
@ -123,12 +143,47 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
||||
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(
|
||||
() => (
|
||||
<Space size={16}>
|
||||
<Button
|
||||
data-testid="cancel-btn"
|
||||
disabled={isFormLoading}
|
||||
size="large"
|
||||
onClick={handleCancel}>
|
||||
{t('label.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="create-btn"
|
||||
htmlType="submit"
|
||||
loading={isFormLoading}
|
||||
size="large"
|
||||
type="primary"
|
||||
onClick={() => form.submit()}>
|
||||
{t('label.create')}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
[isFormLoading, handleCancel, form, t]
|
||||
);
|
||||
|
||||
const fetchTables = useCallback(async (searchValue = '', page = 1) => {
|
||||
try {
|
||||
const response = await searchQuery({
|
||||
@ -326,6 +381,103 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
||||
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 = (
|
||||
<div
|
||||
className={classNames(
|
||||
@ -359,7 +511,6 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
||||
showSearch
|
||||
api={fetchTables}
|
||||
placeholder="Select one or more table at a time"
|
||||
size="large"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@ -379,7 +530,6 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
||||
loading={!selectedTableData}
|
||||
options={columnOptions}
|
||||
placeholder="Select a column"
|
||||
size="large"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
@ -397,26 +547,41 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
||||
options={testTypeOptions}
|
||||
placeholder="Select a test type"
|
||||
popupClassName="no-wrap-option"
|
||||
size="large"
|
||||
onChange={handleTestDefinitionChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{selectedTestDefinition && generateParamsField}
|
||||
</Card>
|
||||
|
||||
<Card className="test-details-card">
|
||||
{generateFormFields(testDetailsFormFields)}
|
||||
|
||||
{isComputeRowCountFieldVisible &&
|
||||
generateFormFields(computeRowCountField)}
|
||||
</Card>
|
||||
</Form>
|
||||
|
||||
{!isDrawer && (
|
||||
<div className="test-case-form-actions">{renderActionButtons}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const drawerFooter = (
|
||||
<div className="drawer-footer-actions">{renderActionButtons}</div>
|
||||
);
|
||||
|
||||
if (isDrawer) {
|
||||
return (
|
||||
<Drawer
|
||||
destroyOnClose
|
||||
open
|
||||
footer={drawerFooter}
|
||||
placement="right"
|
||||
size="large"
|
||||
{...drawerProps}>
|
||||
{formContent}
|
||||
<div className="drawer-form-content">{formContent}</div>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user