From d3eb47983db6a47769c4b54905007a04e88e240f Mon Sep 17 00:00:00 2001 From: Ashish Gupta Date: Fri, 1 Aug 2025 23:32:46 +0530 Subject: [PATCH] Multiple data contract tab and form and detail page improvement (#22700) * multiple data contract tab and form and detail page improvement * pending localization keys * fix the quality icon and semantic listing fix * fix minor issue * fix the schema data not going in api and also fix some styling around add sementic button --- .../svg/DataContractValidationApplication.svg | 21 +- .../ui/src/assets/svg/ic-expand-open.svg | 4 + .../AddDataContract/AddDataContract.tsx | 48 ++- .../ContractDetailFormTab.tsx | 13 +- .../ContractDetailTab/ContractDetail.tsx | 104 +++-- .../ContractDetailTab/contract-detail.less | 38 +- .../ContractQualityFormTab.tsx | 47 ++- .../ContractScehmaFormTab.tsx | 39 +- .../ContractSemanticFormTab.tsx | 355 +++++++++--------- .../contract-semantic-form-tab.less | 30 ++ .../DataContract/ContractTab/ContractTab.tsx | 30 +- .../ui/src/locale/languages/de-de.json | 1 + .../ui/src/locale/languages/en-us.json | 1 + .../ui/src/locale/languages/es-es.json | 1 + .../ui/src/locale/languages/fr-fr.json | 1 + .../ui/src/locale/languages/gl-es.json | 1 + .../ui/src/locale/languages/he-he.json | 1 + .../ui/src/locale/languages/ja-jp.json | 1 + .../ui/src/locale/languages/ko-kr.json | 1 + .../ui/src/locale/languages/mr-in.json | 1 + .../ui/src/locale/languages/nl-nl.json | 1 + .../ui/src/locale/languages/pr-pr.json | 1 + .../ui/src/locale/languages/pt-br.json | 1 + .../ui/src/locale/languages/pt-pt.json | 1 + .../ui/src/locale/languages/ru-ru.json | 1 + .../ui/src/locale/languages/th-th.json | 1 + .../ui/src/locale/languages/tr-tr.json | 1 + .../ui/src/locale/languages/zh-cn.json | 1 + .../utils/DataContract/DataContractUtils.ts | 4 +- .../utils/DataQuality/DataQualityUtils.tsx | 19 +- 30 files changed, 425 insertions(+), 344 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-expand-open.svg diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/DataContractValidationApplication.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/DataContractValidationApplication.svg index 64708284d22..6a4be044f9a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/assets/svg/DataContractValidationApplication.svg +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/DataContractValidationApplication.svg @@ -1,20 +1 @@ - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-expand-open.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-expand-open.svg new file mode 100644 index 00000000000..8b7080b0f12 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-expand-open.svg @@ -0,0 +1,4 @@ + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/AddDataContract/AddDataContract.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/AddDataContract/AddDataContract.tsx index dcae9b6f268..b907790080c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/AddDataContract/AddDataContract.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/AddDataContract/AddDataContract.tsx @@ -22,6 +22,7 @@ import { Typography, } from 'antd'; import { AxiosError } from 'axios'; +import { isEmpty } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ReactComponent as ContractIcon } from '../../../assets/svg/ic-contract.svg'; @@ -34,7 +35,10 @@ import { } from '../../../constants/DataContract.constants'; import { CSMode } from '../../../enums/codemirror.enum'; import { EntityType } from '../../../enums/entity.enum'; -import { DataContract } from '../../../generated/entity/data/dataContract'; +import { + ContractStatus, + DataContract, +} from '../../../generated/entity/data/dataContract'; import { Table } from '../../../generated/entity/data/table'; import { createContract, updateContract } from '../../../rest/contractAPI'; import { getUpdatedContractDetails } from '../../../utils/DataContract/DataContractUtils'; @@ -81,15 +85,24 @@ const AddDataContract: React.FC<{ const handleSave = useCallback(async () => { setIsSubmitting(true); + const validSemantics = formValues.semantics?.filter( + (semantic) => !isEmpty(semantic.name) && !isEmpty(semantic.rule) + ); + try { await (contract - ? updateContract(getUpdatedContractDetails(contract, formValues)) + ? updateContract({ + ...getUpdatedContractDetails(contract, formValues), + semantics: validSemantics, + }) : createContract({ ...formValues, entity: { id: table.id, type: EntityType.TABLE, }, + semantics: validSemantics, + status: ContractStatus.Active, })); showSuccessToast(t('message.data-contract-saved-successfully')); @@ -101,18 +114,20 @@ const AddDataContract: React.FC<{ } }, [contract, formValues]); - const onNext = useCallback( - async (data: Partial) => { + const onFormChange = useCallback( + (data: Partial) => { setFormValues((prev) => ({ ...prev, ...data })); - - setActiveTab((prev) => (Number(prev) + 1).toString()); }, - [activeTab, handleSave] + [setFormValues] ); + const onNext = useCallback(async () => { + setActiveTab((prev) => (Number(prev) + 1).toString()); + }, [setActiveTab]); + const onPrev = useCallback(() => { setActiveTab((prev) => (Number(prev) - 1).toString()); - }, [activeTab]); + }, [setActiveTab]); const items = useMemo( () => [ @@ -126,8 +141,9 @@ const AddDataContract: React.FC<{ key: EDataContractTab.CONTRACT_DETAIL.toString(), children: ( ), @@ -145,8 +161,9 @@ const AddDataContract: React.FC<{ nextLabel={t('label.semantic-plural')} prevLabel={t('label.contract-detail-plural')} selectedSchema={ - formValues.schema?.map((column) => column.name) || [] + contract?.schema?.map((column) => column.name) || [] } + onChange={onFormChange} onNext={onNext} onPrev={onPrev} /> @@ -162,9 +179,10 @@ const AddDataContract: React.FC<{ key: EDataContractTab.SEMANTICS.toString(), children: ( @@ -182,19 +200,17 @@ const AddDataContract: React.FC<{ quality.id ?? '' ) ?? [] } + onChange={onFormChange} onPrev={onPrev} - onUpdate={(qualityExpectations) => - setFormValues((prev) => ({ ...prev, qualityExpectations })) - } /> ), }, ], - [t, onNext, onPrev] + [contract, onFormChange, onNext, onPrev] ); const handleModeChange = useCallback((e: RadioChangeEvent) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailFormTab/ContractDetailFormTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailFormTab/ContractDetailFormTab.tsx index 73817dca409..97ab7544c49 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailFormTab/ContractDetailFormTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailFormTab/ContractDetailFormTab.tsx @@ -26,9 +26,10 @@ import { OwnerLabel } from '../../common/OwnerLabel/OwnerLabel.component'; export const ContractDetailFormTab: React.FC<{ initialValues?: Partial; - onNext: (formData: Partial) => Promise; + onNext: () => void; + onChange: (formData: Partial) => void; nextLabel?: string; -}> = ({ initialValues, onNext, nextLabel }) => { +}> = ({ initialValues, onNext, nextLabel, onChange }) => { const { t } = useTranslation(); const [form] = Form.useForm(); @@ -79,10 +80,6 @@ export const ContractDetailFormTab: React.FC<{ }, ]; - const handleSubmit = () => { - form.submit(); - }; - useEffect(() => { if (initialValues) { form.setFieldsValue({ @@ -110,7 +107,7 @@ export const ContractDetailFormTab: React.FC<{ className="contract-detail-form" form={form} layout="vertical" - onFinish={onNext}> + onValuesChange={onChange}> {generateFormFields(fields)} {owners?.length > 0 && } @@ -118,7 +115,7 @@ export const ContractDetailFormTab: React.FC<{
- diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/ContractDetail.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/ContractDetail.tsx index e1f9384e257..afcafdb1ab7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/ContractDetail.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/ContractDetail.tsx @@ -22,11 +22,13 @@ import { ReactComponent as FlagIcon } from '../../../assets/svg/flag.svg'; import { ReactComponent as CheckIcon } from '../../../assets/svg/ic-check-circle.svg'; import { ReactComponent as DeleteIcon } from '../../../assets/svg/ic-trash.svg'; +import { isEmpty } from 'lodash'; import { Cell, Pie, PieChart } from 'recharts'; import { ICON_DIMENSION, NO_DATA_PLACEHOLDER, } from '../../../constants/constants'; +import { TEST_CASE_STATUS_ICON } from '../../../constants/DataQuality.constants'; import { DEFAULT_SORT_ORDER } from '../../../constants/profiler.constant'; import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum'; import { EntityType } from '../../../enums/entity.enum'; @@ -46,7 +48,6 @@ import { getContractStatusType, getTestCaseSummaryChartItems, } from '../../../utils/DataContract/DataContractUtils'; -import { getTestCaseStatusIcon } from '../../../utils/DataQuality/DataQualityUtils'; import { getRelativeTime } from '../../../utils/date-time/DateTimeUtils'; import { getEntityName } from '../../../utils/EntityUtils'; import { pruneEmptyChildren } from '../../../utils/TableUtils'; @@ -55,6 +56,7 @@ import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import ErrorPlaceHolderNew from '../../common/ErrorWithPlaceholder/ErrorPlaceHolderNew'; import ExpandableCard from '../../common/ExpandableCard/ExpandableCard'; import { OwnerLabel } from '../../common/OwnerLabel/OwnerLabel.component'; +import RichTextEditorPreviewerNew from '../../common/RichTextEditor/RichTextEditorPreviewNew'; import { StatusType } from '../../common/StatusBadge/StatusBadge.interface'; import StatusBadgeV2 from '../../common/StatusBadge/StatusBadgeV2.component'; import Table from '../../common/Table/Table'; @@ -169,6 +171,18 @@ const ContractDetail: React.FC<{ return getTestCaseSummaryChartItems(testCaseSummary); }, [testCaseSummary]); + const getTestCaseStatusIcon = (record: TestCase) => ( + + ); + const handleRunNow = () => { if (contract?.id) { setValidateLoading(true); @@ -183,13 +197,13 @@ const ContractDetail: React.FC<{ }; useEffect(() => { - if (contract?.id && !contract?.latestResult?.resultId) { + if (contract?.id && contract?.latestResult?.resultId) { fetchLatestContractResults(); + } - if (contract?.testSuite?.id) { - fetchTestCaseSummary(); - fetchTestCases(); - } + if (contract?.testSuite?.id) { + fetchTestCaseSummary(); + fetchTestCases(); } }, [contract]); @@ -226,7 +240,7 @@ const ContractDetail: React.FC<{ - {t('message.created-time-ago-by', { + {t('message.modified-time-ago-by', { time: getRelativeTime(contract.updatedAt), by: contract.updatedBy, })} @@ -235,7 +249,7 @@ const ContractDetail: React.FC<{
@@ -250,16 +264,18 @@ const ContractDetail: React.FC<{
-
- {t('label.owner-plural')} - -
+ {!isEmpty(contract.owners) && ( +
+ {t('label.owner-plural')} + +
+ )}
@@ -504,21 +522,25 @@ const ContractDetail: React.FC<{ ))}
- {testCaseResult.map((item) => ( -
- {getTestCaseStatusIcon(item)} -
- - {item.name} - - - {item.description} - + {testCaseResult.map((item) => { + return ( +
+ {getTestCaseStatusIcon(item)} +
+ + {item.name} + + + + +
-
- ))} + ); + })}
)} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/contract-detail.less b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/contract-detail.less index 0918b9a0526..c4bddb01f7e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/contract-detail.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/contract-detail.less @@ -156,25 +156,31 @@ } } -.rule-item { - display: inline-flex; - align-items: center; - gap: 4px; +.rule-item-container { + display: flex; + flex-direction: column; + gap: 8px; - .rule-icon { - font-size: 20px; - margin-right: 4px; - } + .rule-item { + display: inline-flex; + align-items: center; + gap: 4px; - .rule-name { - font-size: 14px; - font-weight: 500; - color: @grey-900; - } + .rule-icon { + font-size: 20px; + margin-right: 4px; + } - .rule-description { - font-size: 12px; - font-weight: 300; + .rule-name { + font-size: 14px; + font-weight: 500; + color: @grey-900; + } + + .rule-description { + font-size: 12px; + font-weight: 300; + } } } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractQualityFormTab/ContractQualityFormTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractQualityFormTab/ContractQualityFormTab.tsx index d4e6737b622..7d28b2a474e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractQualityFormTab/ContractQualityFormTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractQualityFormTab/ContractQualityFormTab.tsx @@ -18,21 +18,27 @@ import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { EntityType } from '../../../enums/entity.enum'; import { Table as TableType } from '../../../generated/entity/data/table'; -import { TestCase } from '../../../generated/tests/testCase'; +import { TestCase, TestCaseStatus } from '../../../generated/tests/testCase'; import { EntityReference } from '../../../generated/type/entityReference'; import { usePaging } from '../../../hooks/paging/usePaging'; import { listTestCases, TestCaseType } from '../../../rest/testAPI'; import { showErrorToast } from '../../../utils/ToastUtils'; import Table from '../../common/Table/Table'; + +import { ColumnsType } from 'antd/lib/table'; +import { toLower } from 'lodash'; +import { DataContract } from '../../../generated/entity/data/dataContract'; +import StatusBadge from '../../common/StatusBadge/StatusBadge.component'; +import { StatusType } from '../../common/StatusBadge/StatusBadge.interface'; import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider'; export const ContractQualityFormTab: React.FC<{ selectedQuality: string[]; - onUpdate: (data: EntityReference[]) => void; + onChange: (data: Partial) => void; onPrev: () => void; prevLabel?: string; -}> = ({ selectedQuality, onUpdate, onPrev, prevLabel }) => { - const [testType, setTestType] = useState<'table' | 'column'>('table'); +}> = ({ selectedQuality, onChange, onPrev, prevLabel }) => { + const [testType, setTestType] = useState(TestCaseType.table); const [allTestCases, setAllTestCases] = useState([]); const { data: table } = useGenericContext(); const { pageSize, handlePagingChange } = usePaging(); @@ -42,7 +48,7 @@ export const ContractQualityFormTab: React.FC<{ ); const { t } = useTranslation(); - const columns = useMemo( + const columns: ColumnsType = useMemo( () => [ { title: t('label.name'), @@ -50,10 +56,20 @@ export const ContractQualityFormTab: React.FC<{ }, { title: t('label.status'), - dataIndex: 'status', + dataIndex: 'testCaseStatus', + key: 'testCaseStatus', + render: (testCaseStatus: TestCaseStatus) => { + return ( + + ); + }, }, ], - [t] + [] ); const fetchAllTests = async () => { @@ -64,8 +80,7 @@ export const ContractQualityFormTab: React.FC<{ try { const { data, paging } = await listTestCases({ entityFQN: table.fullyQualifiedName, - testCaseType: - testType === 'table' ? TestCaseType.table : TestCaseType.column, + testCaseType: testType, limit: pageSize, }); @@ -80,7 +95,7 @@ export const ContractQualityFormTab: React.FC<{ useEffect(() => { fetchAllTests(); - }, []); + }, [testType]); const handleSelection = (selectedRowKeys: string[]) => { const qualityExpectations = selectedRowKeys.map((id) => { @@ -94,7 +109,9 @@ export const ContractQualityFormTab: React.FC<{ } as EntityReference; }); - onUpdate(qualityExpectations); + onChange({ + qualityExpectations, + }); }; return ( @@ -113,8 +130,12 @@ export const ContractQualityFormTab: React.FC<{ className="m-b-sm" value={testType} onChange={(e) => setTestType(e.target.value)}> - {t('label.table')} - {t('label.column')} + + {t('label.table')} + + + {t('label.column')} + ) => void; + onNext: () => void; + onChange: (data: Partial) => void; onPrev: () => void; nextLabel?: string; prevLabel?: string; -}> = ({ selectedSchema, onNext, onPrev, nextLabel, prevLabel }) => { +}> = ({ selectedSchema, onNext, onChange, onPrev, nextLabel, prevLabel }) => { const { t } = useTranslation(); const { fqn } = useFqn(); const [schema, setSchema] = useState([]); - const [selectedKeys, setSelectedKeys] = useState([]); + const [selectedKeys, setSelectedKeys] = useState(selectedSchema); + + const handleChangeTable = useCallback( + (selectedRowKeys: Key[]) => { + setSelectedKeys(selectedRowKeys as string[]); + onChange({ + schema: schema.filter((column) => + selectedRowKeys.includes(column.name) + ), + }); + }, + [schema, onChange] + ); const fetchTableColumns = useCallback(async () => { const response = await getTableColumnsByFQN(fqn); setSchema(pruneEmptyChildren(response.data)); }, [fqn]); - useEffect(() => { - setSelectedKeys(selectedSchema); - }, [selectedSchema]); - useEffect(() => { fetchTableColumns(); }, [fqn]); @@ -160,9 +169,7 @@ export const ContractSchemaFormTab: React.FC<{ rowKey="name" rowSelection={{ selectedRowKeys: selectedKeys, - onChange: (selectedRowKeys) => { - setSelectedKeys(selectedRowKeys as string[]); - }, + onChange: handleChangeTable, }} /> @@ -170,15 +177,7 @@ export const ContractSchemaFormTab: React.FC<{ - diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemanticFormTab/ContractSemanticFormTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemanticFormTab/ContractSemanticFormTab.tsx index 1f45d53c400..7688b912498 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemanticFormTab/ContractSemanticFormTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractSemanticFormTab/ContractSemanticFormTab.tsx @@ -11,22 +11,17 @@ * limitations under the License. */ -import { - ArrowLeftOutlined, - ArrowRightOutlined, - PlusOutlined, -} from '@ant-design/icons'; +import Icon, { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons'; import { FieldErrorProps } from '@rjsf/utils'; import { Button, Col, Form, Input, Row, Switch, Typography } from 'antd'; import Card from 'antd/lib/card/Card'; import TextArea from 'antd/lib/input/TextArea'; -import { useEffect, useState } from 'react'; +import classNames from 'classnames'; +import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { ReactComponent as PlusIcon } from '../../../assets/svg/x-colored.svg'; import { EntityType } from '../../../enums/entity.enum'; -import { - DataContract, - SemanticsRule, -} from '../../../generated/entity/data/dataContract'; +import { DataContract } from '../../../generated/entity/data/dataContract'; import ExpandableCard from '../../common/ExpandableCard/ExpandableCard'; import QueryBuilderWidget from '../../common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget'; import { EditIconButton } from '../../common/IconButtons/EditIconButton'; @@ -34,16 +29,28 @@ import { SearchOutputType } from '../../Explore/AdvanceSearchProvider/AdvanceSea import './contract-semantic-form-tab.less'; export const ContractSemanticFormTab: React.FC<{ - onNext: (data: Partial) => void; + onChange: (data: Partial) => void; + onNext: () => void; onPrev: () => void; initialValues?: Partial; nextLabel?: string; prevLabel?: string; -}> = ({ onNext, onPrev, nextLabel, prevLabel, initialValues }) => { +}> = ({ onChange, onNext, onPrev, nextLabel, prevLabel, initialValues }) => { const { t } = useTranslation(); const [form] = Form.useForm(); const semanticsData = Form.useWatch('semantics', form); const [editingKey, setEditingKey] = useState(null); + const addFunctionRef = useRef<((defaultValue?: any) => void) | null>(null); + + const handleAddSemantic = () => { + addFunctionRef.current?.({ + name: '', + description: '', + rule: '', + enabled: false, + }); + setEditingKey(semanticsData.length); + }; useEffect(() => { form.setFieldsValue({ @@ -58,18 +65,6 @@ export const ContractSemanticFormTab: React.FC<{ }); }, []); - const handleNext = () => { - const semantics = form.getFieldValue('semantics') as SemanticsRule[]; - - const validSemantics = semantics.filter((semantic) => { - return semantic.name && semantic.rule; - }); - - onNext({ - semantics: validSemantics, - }); - }; - useEffect(() => { if (initialValues?.semantics) { form.setFieldsValue({ @@ -80,162 +75,172 @@ export const ContractSemanticFormTab: React.FC<{ return ( <> - -
- - {t('label.semantic-plural')} - - - {t('message.semantics-description')} - + +
+
+ + {t('label.semantic-plural')} + + + {t('message.semantics-description')} + +
+ +
-
+ { + onChange(allValues); + }}> - {(fields, { add }) => ( - <> - {fields.map((field) => { - return ( - - {editingKey === field.key ? null : ( - <> -
- -
- - {semanticsData[field.key]?.name || - t('label.untitled')} - - - {semanticsData[field.key]?.description || - t('label.no-description')} - + {(fields, { add }) => { + // Store the add function so it can be used outside + if (!addFunctionRef.current) { + addFunctionRef.current = add; + } + + return ( + <> + {fields.map((field) => { + return ( + + {editingKey === field.key ? null : ( + <> +
+ +
+ + {semanticsData[field.key]?.name || + t('label.untitled')} + + + {semanticsData[field.key] + ?.description || + t('label.no-description')} + +
-
- setEditingKey(field.key)} + setEditingKey(field.key)} + /> + + )} +
+ ), + }} + key={field.key}> + {editingKey === field.key ? ( + +
+ + + + + + +