mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-06 07:32:46 +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';
|
@import (reference) '../../../../styles/variables.less';
|
||||||
|
|
||||||
.test-case-form-v1 {
|
.test-case-form-v1 {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-bottom: 80px; // Space for fixed buttons
|
||||||
|
|
||||||
.ant-form-item {
|
.ant-form-item {
|
||||||
margin-bottom: @size-lg;
|
margin-bottom: @size-lg;
|
||||||
|
|
||||||
@ -28,7 +32,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.select-table-card,
|
.select-table-card,
|
||||||
.test-type-card {
|
.test-type-card,
|
||||||
|
.test-details-card {
|
||||||
background-color: @grey-1;
|
background-color: @grey-1;
|
||||||
border: 1px solid @grey-200;
|
border: 1px solid @grey-200;
|
||||||
border-radius: @border-radius-sm;
|
border-radius: @border-radius-sm;
|
||||||
@ -51,9 +56,17 @@
|
|||||||
font-weight: @font-medium;
|
font-weight: @font-medium;
|
||||||
font-size: @font-size-base;
|
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;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,4 +87,44 @@
|
|||||||
font-size: @font-size-base;
|
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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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 { useForm } from 'antd/lib/form/Form';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
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 ColumnIcon } from '../../../../assets/svg/ic-column.svg';
|
||||||
import { ReactComponent as TableIcon } from '../../../../assets/svg/ic-format-table.svg';
|
import { ReactComponent as TableIcon } from '../../../../assets/svg/ic-format-table.svg';
|
||||||
import {
|
import {
|
||||||
PAGE_SIZE_LARGE,
|
PAGE_SIZE_LARGE,
|
||||||
PAGE_SIZE_MEDIUM,
|
PAGE_SIZE_MEDIUM,
|
||||||
} from '../../../../constants/constants';
|
} from '../../../../constants/constants';
|
||||||
|
import { ENTITY_NAME_REGEX } from '../../../../constants/regex.constants';
|
||||||
import { SearchIndex } from '../../../../enums/search.enum';
|
import { SearchIndex } from '../../../../enums/search.enum';
|
||||||
|
import { TagSource } from '../../../../generated/api/domains/createDataProduct';
|
||||||
import { Table } from '../../../../generated/entity/data/table';
|
import { Table } from '../../../../generated/entity/data/table';
|
||||||
|
import { TagLabel } from '../../../../generated/tests/testCase';
|
||||||
import {
|
import {
|
||||||
EntityType,
|
EntityType,
|
||||||
TestDefinition,
|
TestDefinition,
|
||||||
TestPlatform,
|
TestPlatform,
|
||||||
} from '../../../../generated/tests/testDefinition';
|
} from '../../../../generated/tests/testDefinition';
|
||||||
|
import {
|
||||||
|
FieldProp,
|
||||||
|
FieldTypes,
|
||||||
|
FormItemLayout,
|
||||||
|
} from '../../../../interface/FormUtils.interface';
|
||||||
import { TableSearchSource } from '../../../../interface/search.interface';
|
import { TableSearchSource } from '../../../../interface/search.interface';
|
||||||
import { searchQuery } from '../../../../rest/searchAPI';
|
import { searchQuery } from '../../../../rest/searchAPI';
|
||||||
import { getTableDetailsByFQN } from '../../../../rest/tableAPI';
|
import { getTableDetailsByFQN } from '../../../../rest/tableAPI';
|
||||||
import { getListTestDefinitions } from '../../../../rest/testAPI';
|
import { getListTestDefinitions } from '../../../../rest/testAPI';
|
||||||
import { filterSelectOptions } from '../../../../utils/CommonUtils';
|
import { filterSelectOptions } from '../../../../utils/CommonUtils';
|
||||||
import { getEntityName } from '../../../../utils/EntityUtils';
|
import { getEntityName } from '../../../../utils/EntityUtils';
|
||||||
|
import { generateFormFields } from '../../../../utils/formUtils';
|
||||||
import { AsyncSelect } from '../../../common/AsyncSelect/AsyncSelect';
|
import { AsyncSelect } from '../../../common/AsyncSelect/AsyncSelect';
|
||||||
import SelectionCardGroup from '../../../common/SelectionCardGroup/SelectionCardGroup';
|
import SelectionCardGroup from '../../../common/SelectionCardGroup/SelectionCardGroup';
|
||||||
import { SelectionOption } from '../../../common/SelectionCardGroup/SelectionCardGroup.interface';
|
import { SelectionOption } from '../../../common/SelectionCardGroup/SelectionCardGroup.interface';
|
||||||
@ -45,7 +64,9 @@ export interface TestCaseFormV1Props {
|
|||||||
className?: string;
|
className?: string;
|
||||||
table?: Table;
|
table?: Table;
|
||||||
onFormSubmit?: (values: FormValues) => void;
|
onFormSubmit?: (values: FormValues) => void;
|
||||||
|
onCancel?: () => void;
|
||||||
initialValues?: Partial<FormValues>;
|
initialValues?: Partial<FormValues>;
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
@ -53,6 +74,11 @@ interface FormValues {
|
|||||||
selectedTable?: string;
|
selectedTable?: string;
|
||||||
selectedColumn?: string;
|
selectedColumn?: string;
|
||||||
testTypeId?: string;
|
testTypeId?: string;
|
||||||
|
testName?: string;
|
||||||
|
description?: string;
|
||||||
|
tags?: TagLabel[];
|
||||||
|
glossaryTerms?: TagLabel[];
|
||||||
|
computePassedFailedRowCount?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TablesCache = Map<string, TableSearchSource>;
|
type TablesCache = Map<string, TableSearchSource>;
|
||||||
@ -92,16 +118,6 @@ const convertSearchSourceToTable = (searchSource: TableSearchSource): Table =>
|
|||||||
columns: searchSource.columns || [],
|
columns: searchSource.columns || [],
|
||||||
} as Table);
|
} 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> = ({
|
const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
||||||
className,
|
className,
|
||||||
drawerProps,
|
drawerProps,
|
||||||
@ -109,8 +125,12 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
|||||||
isDrawer = false,
|
isDrawer = false,
|
||||||
table,
|
table,
|
||||||
onFormSubmit,
|
onFormSubmit,
|
||||||
|
onCancel,
|
||||||
|
loading: externalLoading = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [form] = useForm<FormValues>();
|
const [form] = useForm<FormValues>();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const [testDefinitions, setTestDefinitions] = useState<TestDefinition[]>([]);
|
const [testDefinitions, setTestDefinitions] = useState<TestDefinition[]>([]);
|
||||||
const [selectedTestDefinition, setSelectedTestDefinition] =
|
const [selectedTestDefinition, setSelectedTestDefinition] =
|
||||||
useState<TestDefinition>();
|
useState<TestDefinition>();
|
||||||
@ -123,12 +143,47 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
|||||||
const selectedTestLevel = Form.useWatch('testLevel', form);
|
const selectedTestLevel = Form.useWatch('testLevel', form);
|
||||||
const selectedTable = Form.useWatch('selectedTable', form);
|
const selectedTable = Form.useWatch('selectedTable', form);
|
||||||
const selectedColumn = Form.useWatch('selectedColumn', form);
|
const selectedColumn = Form.useWatch('selectedColumn', form);
|
||||||
const selectedTestType = Form.useWatch('testTypeId', form);
|
|
||||||
|
|
||||||
const handleSubmit = (values: FormValues) => {
|
const handleSubmit = async (values: FormValues) => {
|
||||||
onFormSubmit?.(values);
|
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) => {
|
const fetchTables = useCallback(async (searchValue = '', page = 1) => {
|
||||||
try {
|
try {
|
||||||
const response = await searchQuery({
|
const response = await searchQuery({
|
||||||
@ -326,6 +381,103 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
|||||||
return null;
|
return null;
|
||||||
}, [selectedTestDefinition, selectedTableData]);
|
}, [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 = (
|
const formContent = (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@ -359,7 +511,6 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
|||||||
showSearch
|
showSearch
|
||||||
api={fetchTables}
|
api={fetchTables}
|
||||||
placeholder="Select one or more table at a time"
|
placeholder="Select one or more table at a time"
|
||||||
size="large"
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@ -379,7 +530,6 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
|||||||
loading={!selectedTableData}
|
loading={!selectedTableData}
|
||||||
options={columnOptions}
|
options={columnOptions}
|
||||||
placeholder="Select a column"
|
placeholder="Select a column"
|
||||||
size="large"
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
@ -397,26 +547,41 @@ const TestCaseFormV1: FC<TestCaseFormV1Props> = ({
|
|||||||
options={testTypeOptions}
|
options={testTypeOptions}
|
||||||
placeholder="Select a test type"
|
placeholder="Select a test type"
|
||||||
popupClassName="no-wrap-option"
|
popupClassName="no-wrap-option"
|
||||||
size="large"
|
|
||||||
onChange={handleTestDefinitionChange}
|
onChange={handleTestDefinitionChange}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{selectedTestDefinition && generateParamsField}
|
{selectedTestDefinition && generateParamsField}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card className="test-details-card">
|
||||||
|
{generateFormFields(testDetailsFormFields)}
|
||||||
|
|
||||||
|
{isComputeRowCountFieldVisible &&
|
||||||
|
generateFormFields(computeRowCountField)}
|
||||||
|
</Card>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
{!isDrawer && (
|
||||||
|
<div className="test-case-form-actions">{renderActionButtons}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const drawerFooter = (
|
||||||
|
<div className="drawer-footer-actions">{renderActionButtons}</div>
|
||||||
|
);
|
||||||
|
|
||||||
if (isDrawer) {
|
if (isDrawer) {
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
open
|
open
|
||||||
|
footer={drawerFooter}
|
||||||
placement="right"
|
placement="right"
|
||||||
size="large"
|
size="large"
|
||||||
{...drawerProps}>
|
{...drawerProps}>
|
||||||
{formContent}
|
<div className="drawer-form-content">{formContent}</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user