mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-04 19:44:58 +00:00
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
This commit is contained in:
parent
a4b0833633
commit
d3eb47983d
@ -1,20 +1 @@
|
||||
<svg width="55" height="55" viewBox="0 0 55 55" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="55" height="38" rx="4" fill="#FBF2DB"/>
|
||||
<path d="M50.0342 51.2431H3.96581C3.43237 51.2431 3 50.8107 3 50.2772V49.0869C3 48.5535 3.43237 48.1211 3.96581 48.1211H50.0342C50.5676 48.1211 51 48.5535 51 49.0869V50.2772C51 50.8107 50.5676 51.2431 50.0342 51.2431Z" fill="#3D4FC3"/>
|
||||
<path d="M50.0341 51.2431H48.6584V48.1211H50.0341C50.5676 48.1211 50.9999 48.5535 50.9999 49.0869V50.2772C50.9999 50.8107 50.5676 51.2431 50.0341 51.2431Z" fill="#3342AA"/>
|
||||
<path d="M10.4245 35.0469H6.11217C5.57874 35.0469 5.14636 35.4793 5.14636 36.0127V48.12H11.3903V36.0127C11.3903 35.4793 10.9579 35.0469 10.4245 35.0469Z" fill="#9DC6FB"/>
|
||||
<path d="M10.4245 35.0469H9.24402V48.12H11.3903V36.0127C11.3903 35.4793 10.958 35.0469 10.4245 35.0469Z" fill="#80B4FB"/>
|
||||
<path d="M22.9122 22.3672H18.5998C18.0664 22.3672 17.634 22.7996 17.634 23.333V48.1233H23.878V23.333C23.878 22.7996 23.4456 22.3672 22.9122 22.3672Z" fill="#AC80DD"/>
|
||||
<path d="M22.9122 22.3672H21.7317V48.1233H23.878V23.333C23.878 22.7996 23.4456 22.3672 22.9122 22.3672Z" fill="#9D66D5"/>
|
||||
<path d="M35.4001 29.1953H31.0878C30.5543 29.1953 30.1219 29.6277 30.1219 30.1611V48.1221H36.3659V30.1611C36.3658 29.6277 35.9334 29.1953 35.4001 29.1953Z" fill="#E490D0"/>
|
||||
<path d="M35.4001 29.1953H34.2196V48.1221H36.3659V30.1611C36.3658 29.6277 35.9334 29.1953 35.4001 29.1953Z" fill="#DF73C1"/>
|
||||
<path d="M47.888 17.4883H43.5757C43.0422 17.4883 42.6099 17.9207 42.6099 18.4541V48.1224H48.8538V18.4541C48.8538 17.9207 48.4213 17.4883 47.888 17.4883Z" fill="#B3E59F"/>
|
||||
<path d="M47.8879 17.4883H46.7074V48.1224H48.8537V18.4541C48.8537 17.9207 48.4212 17.4883 47.8879 17.4883Z" fill="#95D6A4"/>
|
||||
<path d="M33.2433 20.5556C33.1258 20.5556 33.0068 20.527 32.8964 20.4666L20.4086 13.6374C20.0576 13.4455 19.9287 13.0053 20.1207 12.6543C20.3126 12.3033 20.7526 12.1742 21.1038 12.3664L33.5916 19.1956C33.9425 19.3876 34.0715 19.8277 33.8795 20.1787C33.7479 20.4194 33.4995 20.5556 33.2433 20.5556Z" fill="#FFC250"/>
|
||||
<path d="M33.2441 20.5505C33.0509 20.5505 32.8581 20.4737 32.7155 20.3216C32.4418 20.0297 32.4566 19.5713 32.7485 19.2977L45.2363 7.59044C45.5282 7.31678 45.9865 7.33169 46.2602 7.62344C46.5338 7.91538 46.519 8.37372 46.2272 8.64738L33.7393 20.3546C33.5995 20.4856 33.4216 20.5505 33.2441 20.5505Z" fill="#FFC250"/>
|
||||
<path d="M8.26839 26.4089C8.08492 26.4089 7.90136 26.3397 7.76017 26.2008C7.47517 25.9201 7.47161 25.4615 7.7522 25.1764L20.2401 12.4935C20.5207 12.2083 20.9793 12.205 21.2645 12.4855C21.5495 12.7662 21.553 13.2248 21.2725 13.5099L8.78458 26.1927C8.64283 26.3368 8.45561 26.4089 8.26839 26.4089Z" fill="#FFC250"/>
|
||||
<path d="M8.26833 28.8025C9.99255 28.8025 11.3903 27.4048 11.3903 25.6806C11.3903 23.9563 9.99255 22.5586 8.26833 22.5586C6.54412 22.5586 5.14636 23.9563 5.14636 25.6806C5.14636 27.4048 6.54412 28.8025 8.26833 28.8025Z" fill="#FFD064"/>
|
||||
<path d="M20.7561 16.1228C22.4803 16.1228 23.8781 14.7251 23.8781 13.0009C23.8781 11.2767 22.4803 9.87891 20.7561 9.87891C19.0319 9.87891 17.6342 11.2767 17.6342 13.0009C17.6342 14.7251 19.0319 16.1228 20.7561 16.1228Z" fill="#FFD064"/>
|
||||
<path d="M33.2439 22.951C34.9681 22.951 36.3659 21.5532 36.3659 19.829C36.3659 18.1048 34.9681 16.707 33.2439 16.707C31.5197 16.707 30.1219 18.1048 30.1219 19.829C30.1219 21.5532 31.5197 22.951 33.2439 22.951Z" fill="#FFD064"/>
|
||||
<path d="M45.7318 11.2439C47.456 11.2439 48.8538 9.84618 48.8538 8.12197C48.8538 6.39775 47.456 5 45.7318 5C44.0076 5 42.6099 6.39775 42.6099 8.12197C42.6099 9.84618 44.0076 11.2439 45.7318 11.2439Z" fill="#FFD064"/>
|
||||
</svg>
|
||||
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="linear-gradient" gradientUnits="userSpaceOnUse" x1="449.13" x2="116.43" y1="252.97" y2="252.97"><stop offset="0" stop-color="#cacfdd"/><stop offset="1" stop-color="#eef0f1"/></linearGradient><linearGradient id="linear-gradient-2" x1="425.65" x2="74.52" xlink:href="#linear-gradient" y1="284.2" y2="284.2"/><linearGradient id="linear-gradient-3" gradientUnits="userSpaceOnUse" x1="110.93" x2="165.31" y1="450.67" y2="450.67"><stop offset="0" stop-color="#a9afc4"/><stop offset="1" stop-color="#d4d8e5"/></linearGradient><linearGradient id="linear-gradient-4" x1="110.93" x2="188.22" xlink:href="#linear-gradient" y1="208.03" y2="208.03"/><g isolation="isolate"><g id="File_Document" data-name="File Document"><path d="m143 71.29a12.68 12.68 0 0 1 -12.5-10.7c-1.78-11.19 10.92-18.84 20-12.25a12.69 12.69 0 0 1 -7.49 23zm-6.58-11.64a6.69 6.69 0 1 0 10.58-6.46 6.68 6.68 0 0 0 -10.55 6.46z" fill="#5e6175"/><path d="m101.91 107.5a9 9 0 1 1 5.31-16.29c6.98 5.07 3.46 16.29-5.31 16.29zm-2.91-8.5a3 3 0 1 0 4.74-2.9 3 3 0 0 0 -4.74 2.9z" fill="#5e6175"/><path d="m429.14 55.31h19.07v6h-19.07z" fill="#5e6175" transform="matrix(.707 -.707 .707 .707 87.22 327.23)"/><path d="m413.21 36h6v19.06h-6z" fill="#5e6175"/><path d="m442.44 76.62h19.06v6h-19.06z" fill="#5e6175"/><path d="m122.43 471.52-4.24-4.25 4.24-4.24 4.25 4.24zm-11.31-11.32-4.24-4.2 4.24-4.24 4.24 4.24zm-11.31-11.31c-2.6-2.6-3.33-3.29-4.34-4.46l4.54-3.92c.89 1 1.39 1.49 4 4.14zm-10-13.89a32.5 32.5 0 0 1 -1.76-6.37l5.92-1a26.61 26.61 0 0 0 1.43 5.19zm3.79-16.8h-6v-6h6zm0-16h-6v-6h6zm0-16h-6v-6h6zm0-16h-6v-6h6z" fill="#a9afc4"/><path d="m419.85 76.71v336.83h-196.47l-49.99-47.38v-289.45z" fill="#f5f5f5"/><path d="m404.16 92.4v321.14h-180.78l-49.99-47.37v-273.77z" fill="url(#linear-gradient)" mix-blend-mode="multiply"/><path d="m422.85 416.54h-200.67l-51.79-49.09v-293.74h252.46zm-198.27-6h192.27v-330.83h-240.46v285.16z" fill="#5e6175"/><path d="m388.62 107.94v336.83h-196.47l-49.99-47.38v-289.45z" fill="#f5f5f5"/><path d="m372.93 123.63v321.13h-180.78l-49.99-47.37v-273.76z" fill="url(#linear-gradient-2)" mix-blend-mode="multiply"/><path d="m391.62 447.77h-200.62l-51.79-49.09v-293.74h252.41zm-198.27-6h192.27v-330.83h-240.46v285.16z" fill="#5e6175"/><path d="m357.39 139.17v336.83h-196.47l-49.99-47.38v-289.45z" fill="#f5f5f5"/><g fill="#dee1ec"><path d="m193.27 191.12h30.82v13.8h-30.82z"/><path d="m244.85 191.12h75.22v13.8h-75.22z"/><path d="m184.71 229.08h135.36v13.8h-135.36z"/><path d="m152.75 267.05h112.64v13.8h-112.64z"/><path d="m286.19 267.05h33.87v13.8h-33.87z"/><path d="m152.75 305.01h167.31v13.8h-167.31z"/><path d="m152.75 342.98h48.27v13.8h-48.27z"/><path d="m221.2 342.98h98.87v13.8h-98.87z"/><path d="m152.75 380.94h167.31v13.8h-167.31z"/><path d="m261.03 418.91h59.03v13.8h-59.03z"/></g><path d="m165.31 425.29v50.71h-4.13c-.39 0 3.61 3.67-50.25-47.38v-3.33z" fill="url(#linear-gradient-3)"/><path d="m160.93 479.1c-1.55 0 .8 1.81-53-49.19v-7.62h60.38v56.71h-6.6a3.21 3.21 0 0 1 -.78.1zm1.21-6.1c.27 0 .17 3.45.17-44.71h-47.37c34.23 32.44 44.48 42.21 47.2 44.71z" fill="#5e6175"/><path d="m188.22 200.26a76.72 76.72 0 0 1 -76.63 76.63h-.66v-137.72h46.86a76.5 76.5 0 0 1 30.43 61.09z" fill="url(#linear-gradient-4)" mix-blend-mode="multiply"/><path d="m360.39 479h-200.67l-51.79-49.09v-293.74h252.46zm-198.28-6h192.28v-330.83h-240.46v285.16z" fill="#5e6175"/><circle cx="111.59" cy="200.26" fill="#f5f5f5" r="61.09"/><path d="m111.59 264.35a64.09 64.09 0 1 1 64.09-64.09 64.16 64.16 0 0 1 -64.09 64.09zm0-122.18a58.09 58.09 0 1 0 58.09 58.09 58.16 58.16 0 0 0 -58.09-58.09z" fill="#5e6175"/><path d="m100.97 229.12-25.66-25.67 8.86-8.87 16.8 16.8 38.04-38.03 8.87 8.87z" fill="#e1830e"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.7 KiB |
@ -0,0 +1,4 @@
|
||||
|
||||
<svg viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1V13M7 1H11.6667C12.0203 1 12.3594 1.14048 12.6095 1.39052C12.8595 1.64057 13 1.97971 13 2.33333V11.6667C13 12.0203 12.8595 12.3594 12.6095 12.6095C12.3594 12.8595 12.0203 13 11.6667 13H7V1ZM7 1H2.33333C1.97971 1 1.64057 1.14048 1.39052 1.39052C1.14048 1.64057 1 1.97971 1 2.33333V11.6667C1 12.0203 1.14048 12.3594 1.39052 12.6095C1.64057 12.8595 1.97971 13 2.33333 13H7V1Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 562 B |
@ -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<DataContract>) => {
|
||||
const onFormChange = useCallback(
|
||||
(data: Partial<DataContract>) => {
|
||||
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: (
|
||||
<ContractDetailFormTab
|
||||
initialValues={formValues}
|
||||
initialValues={contract}
|
||||
nextLabel={t('label.schema')}
|
||||
onChange={onFormChange}
|
||||
onNext={onNext}
|
||||
/>
|
||||
),
|
||||
@ -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: (
|
||||
<ContractSemanticFormTab
|
||||
initialValues={formValues}
|
||||
initialValues={contract}
|
||||
nextLabel={t('label.quality')}
|
||||
prevLabel={t('label.schema')}
|
||||
onChange={onFormChange}
|
||||
onNext={onNext}
|
||||
onPrev={onPrev}
|
||||
/>
|
||||
@ -182,19 +200,17 @@ const AddDataContract: React.FC<{
|
||||
<ContractQualityFormTab
|
||||
prevLabel={t('label.semantic-plural')}
|
||||
selectedQuality={
|
||||
formValues.qualityExpectations?.map(
|
||||
contract?.qualityExpectations?.map(
|
||||
(quality) => quality.id ?? ''
|
||||
) ?? []
|
||||
}
|
||||
onChange={onFormChange}
|
||||
onPrev={onPrev}
|
||||
onUpdate={(qualityExpectations) =>
|
||||
setFormValues((prev) => ({ ...prev, qualityExpectations }))
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[t, onNext, onPrev]
|
||||
[contract, onFormChange, onNext, onPrev]
|
||||
);
|
||||
|
||||
const handleModeChange = useCallback((e: RadioChangeEvent) => {
|
||||
|
||||
@ -26,9 +26,10 @@ import { OwnerLabel } from '../../common/OwnerLabel/OwnerLabel.component';
|
||||
|
||||
export const ContractDetailFormTab: React.FC<{
|
||||
initialValues?: Partial<DataContract>;
|
||||
onNext: (formData: Partial<DataContract>) => Promise<void>;
|
||||
onNext: () => void;
|
||||
onChange: (formData: Partial<DataContract>) => 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 && <OwnerLabel owners={owners} />}
|
||||
@ -118,7 +115,7 @@ export const ContractDetailFormTab: React.FC<{
|
||||
</div>
|
||||
</Card>
|
||||
<div className="d-flex justify-end m-t-md">
|
||||
<Button htmlType="submit" type="primary" onClick={handleSubmit}>
|
||||
<Button htmlType="submit" type="primary" onClick={onNext}>
|
||||
{nextLabel ?? t('label.next')}
|
||||
<ArrowRightOutlined />
|
||||
</Button>
|
||||
|
||||
@ -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) => (
|
||||
<Icon
|
||||
className="test-status-icon"
|
||||
component={
|
||||
TEST_CASE_STATUS_ICON[
|
||||
(record?.testCaseResult?.testCaseStatus ??
|
||||
'Queued') as keyof typeof TEST_CASE_STATUS_ICON
|
||||
]
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
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<{
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Text className="contract-time">
|
||||
{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<{
|
||||
<div className="d-flex items-center gap-2 m-t-xs">
|
||||
<StatusBadgeV2
|
||||
externalIcon={FlagIcon}
|
||||
label={t('label.active')}
|
||||
label={contract.status ?? t('label.active')}
|
||||
status={StatusType.Success}
|
||||
/>
|
||||
|
||||
@ -250,16 +264,18 @@ const ContractDetail: React.FC<{
|
||||
</Col>
|
||||
<Col>
|
||||
<div className="contract-action-container">
|
||||
<div className="contract-owner-label-container">
|
||||
<Typography.Text>{t('label.owner-plural')}</Typography.Text>
|
||||
<OwnerLabel
|
||||
avatarSize={24}
|
||||
isCompactView={false}
|
||||
maxVisibleOwners={5}
|
||||
owners={contract.owners}
|
||||
showLabel={false}
|
||||
/>
|
||||
</div>
|
||||
{!isEmpty(contract.owners) && (
|
||||
<div className="contract-owner-label-container">
|
||||
<Typography.Text>{t('label.owner-plural')}</Typography.Text>
|
||||
<OwnerLabel
|
||||
avatarSize={24}
|
||||
isCompactView={false}
|
||||
maxVisibleOwners={5}
|
||||
owners={contract.owners}
|
||||
showLabel={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="contract-run-now-button"
|
||||
@ -431,15 +447,17 @@ const ContractDetail: React.FC<{
|
||||
<Typography.Text className="card-subtitle">
|
||||
{t('label.custom-integrity-rules')}
|
||||
</Typography.Text>
|
||||
{(contract?.semantics ?? []).map((item) => (
|
||||
<div className="rule-item">
|
||||
<Icon className="rule-icon" component={CheckIcon} />
|
||||
<span className="rule-name">{item.name}</span>{' '}
|
||||
<span className="rule-description">
|
||||
{item.description}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="rule-item-container">
|
||||
{(contract?.semantics ?? []).map((item) => (
|
||||
<div className="rule-item">
|
||||
<Icon className="rule-icon" component={CheckIcon} />
|
||||
<span className="rule-name">{item.name}</span>{' '}
|
||||
<span className="rule-description">
|
||||
{item.description}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ExpandableCard>
|
||||
</Col>
|
||||
@ -504,21 +522,25 @@ const ContractDetail: React.FC<{
|
||||
))}
|
||||
</div>
|
||||
<Space direction="vertical">
|
||||
{testCaseResult.map((item) => (
|
||||
<div
|
||||
className="data-quality-item d-flex items-center"
|
||||
key={item.id}>
|
||||
{getTestCaseStatusIcon(item)}
|
||||
<div className="data-quality-item-content">
|
||||
<Typography.Text className="data-quality-item-name">
|
||||
{item.name}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="data-quality-item-description">
|
||||
{item.description}
|
||||
</Typography.Text>
|
||||
{testCaseResult.map((item) => {
|
||||
return (
|
||||
<div
|
||||
className="data-quality-item d-flex items-center"
|
||||
key={item.id}>
|
||||
{getTestCaseStatusIcon(item)}
|
||||
<div className="data-quality-item-content">
|
||||
<Typography.Text className="data-quality-item-name">
|
||||
{item.name}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="data-quality-item-description">
|
||||
<RichTextEditorPreviewerNew
|
||||
markdown={item.description ?? ''}
|
||||
/>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<DataContract>) => 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>(TestCaseType.table);
|
||||
const [allTestCases, setAllTestCases] = useState<TestCase[]>([]);
|
||||
const { data: table } = useGenericContext<TableType>();
|
||||
const { pageSize, handlePagingChange } = usePaging();
|
||||
@ -42,7 +48,7 @@ export const ContractQualityFormTab: React.FC<{
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = useMemo(
|
||||
const columns: ColumnsType<TestCase> = 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 (
|
||||
<StatusBadge
|
||||
dataTestId={`status-badge-${testCaseStatus}`}
|
||||
label={testCaseStatus}
|
||||
status={toLower(testCaseStatus) as StatusType}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[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)}>
|
||||
<Radio.Button value="table">{t('label.table')}</Radio.Button>
|
||||
<Radio.Button value="column">{t('label.column')}</Radio.Button>
|
||||
<Radio.Button value={TestCaseType.table}>
|
||||
{t('label.table')}
|
||||
</Radio.Button>
|
||||
<Radio.Button value={TestCaseType.column}>
|
||||
{t('label.column')}
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Table
|
||||
columns={columns}
|
||||
|
||||
@ -14,7 +14,7 @@ import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Key, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NO_DATA_PLACEHOLDER } from '../../../constants/constants';
|
||||
import { TABLE_COLUMNS_KEYS } from '../../../constants/TableKeys.constants';
|
||||
@ -36,25 +36,34 @@ import TableTags from '../../Database/TableTags/TableTags.component';
|
||||
|
||||
export const ContractSchemaFormTab: React.FC<{
|
||||
selectedSchema: string[];
|
||||
onNext: (data: Partial<DataContract>) => void;
|
||||
onNext: () => void;
|
||||
onChange: (data: Partial<DataContract>) => 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<Column[]>([]);
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>(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,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
@ -170,15 +177,7 @@ export const ContractSchemaFormTab: React.FC<{
|
||||
<Button icon={<ArrowLeftOutlined />} type="default" onClick={onPrev}>
|
||||
{prevLabel ?? t('label.previous')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() =>
|
||||
onNext({
|
||||
schema: schema.filter((column) =>
|
||||
selectedKeys.includes(column.name)
|
||||
),
|
||||
})
|
||||
}>
|
||||
<Button type="primary" onClick={onNext}>
|
||||
{nextLabel ?? t('label.next')}
|
||||
<ArrowRightOutlined />
|
||||
</Button>
|
||||
|
||||
@ -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<DataContract>) => void;
|
||||
onChange: (data: Partial<DataContract>) => void;
|
||||
onNext: () => void;
|
||||
onPrev: () => void;
|
||||
initialValues?: Partial<DataContract>;
|
||||
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<number | null>(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 (
|
||||
<>
|
||||
<Card className="container bg-grey p-box">
|
||||
<div>
|
||||
<Typography.Text className="contract-detail-form-tab-title">
|
||||
{t('label.semantic-plural')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="contract-detail-form-tab-description">
|
||||
{t('message.semantics-description')}
|
||||
</Typography.Text>
|
||||
<Card className="contract-semantic-form-container container bg-grey p-box">
|
||||
<div className="d-flex justify-between items-center">
|
||||
<div>
|
||||
<Typography.Text className="contract-detail-form-tab-title">
|
||||
{t('label.semantic-plural')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="contract-detail-form-tab-description">
|
||||
{t('message.semantics-description')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="add-semantic-button"
|
||||
disabled={!!editingKey || !addFunctionRef.current}
|
||||
icon={<Icon className="anticon" component={PlusIcon} />}
|
||||
type="link"
|
||||
onClick={handleAddSemantic}>
|
||||
{t('label.add-entity', {
|
||||
entity: t('label.semantic-plural'),
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Form form={form} layout="vertical">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onValuesChange={(_, allValues) => {
|
||||
onChange(allValues);
|
||||
}}>
|
||||
<Form.List name="semantics">
|
||||
{(fields, { add }) => (
|
||||
<>
|
||||
{fields.map((field) => {
|
||||
return (
|
||||
<ExpandableCard
|
||||
cardProps={{
|
||||
className: 'm-t-md',
|
||||
title: (
|
||||
<div className="w-full d-flex justify-between items-center">
|
||||
{editingKey === field.key ? null : (
|
||||
<>
|
||||
<div className="d-flex items-center gap-6">
|
||||
<Switch
|
||||
checked={semanticsData[field.key].enabled}
|
||||
/>
|
||||
<div className="d-flex flex-column">
|
||||
<Typography.Text>
|
||||
{semanticsData[field.key]?.name ||
|
||||
t('label.untitled')}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{semanticsData[field.key]?.description ||
|
||||
t('label.no-description')}
|
||||
</Typography.Text>
|
||||
{(fields, { add }) => {
|
||||
// Store the add function so it can be used outside
|
||||
if (!addFunctionRef.current) {
|
||||
addFunctionRef.current = add;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{fields.map((field) => {
|
||||
return (
|
||||
<ExpandableCard
|
||||
cardProps={{
|
||||
className: classNames('expandable-card m-t-md', {
|
||||
'expanded-active-card': editingKey === field.key,
|
||||
}),
|
||||
title: (
|
||||
<div className="w-full d-flex justify-between items-center">
|
||||
{editingKey === field.key ? null : (
|
||||
<>
|
||||
<div className="d-flex items-center gap-6">
|
||||
<Switch
|
||||
checked={semanticsData[field.key].enabled}
|
||||
/>
|
||||
<div className="d-flex flex-column">
|
||||
<Typography.Text>
|
||||
{semanticsData[field.key]?.name ||
|
||||
t('label.untitled')}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{semanticsData[field.key]
|
||||
?.description ||
|
||||
t('label.no-description')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EditIconButton
|
||||
newLook
|
||||
data-testid={`edit-semantic=${field.key}`}
|
||||
size="small"
|
||||
onClick={() => setEditingKey(field.key)}
|
||||
<EditIconButton
|
||||
newLook
|
||||
data-testid={`edit-semantic=${field.key}`}
|
||||
size="small"
|
||||
onClick={() => setEditingKey(field.key)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
key={field.key}>
|
||||
{editingKey === field.key ? (
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={t('label.name')}
|
||||
name={[field.name, 'name']}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={t('label.description')}
|
||||
name={[field.name, 'description']}>
|
||||
<TextArea />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={t('label.enabled')}
|
||||
name={[field.name, 'enabled']}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={t('label.add-entity', {
|
||||
entity: t('label.rule'),
|
||||
})}
|
||||
name={[field.name, 'rule']}>
|
||||
{/* @ts-expect-error because Form.Item will provide value and onChange */}
|
||||
<QueryBuilderWidget
|
||||
formContext={{
|
||||
entityType: EntityType.TABLE,
|
||||
}}
|
||||
id="rule"
|
||||
label={t('label.rule')}
|
||||
name={`${field.name}.rule`}
|
||||
options={{
|
||||
addButtonText: t('label.add-semantic'),
|
||||
removeButtonText: t(
|
||||
'label.remove-semantic'
|
||||
),
|
||||
}}
|
||||
registry={{} as FieldErrorProps['registry']}
|
||||
schema={{
|
||||
outputType: SearchOutputType.JSONLogic,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col className="d-flex justify-end" span={24}>
|
||||
<Button onClick={() => setEditingKey(null)}>
|
||||
{t('label.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className="m-l-md"
|
||||
type="primary"
|
||||
onClick={() => setEditingKey(null)}>
|
||||
{t('label.save')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
) : (
|
||||
<div className="semantic-rule-editor-view-only">
|
||||
{/* @ts-expect-error because Form.Item will provide value and onChange */}
|
||||
<QueryBuilderWidget
|
||||
readonly
|
||||
formContext={{
|
||||
entityType: EntityType.TABLE,
|
||||
}}
|
||||
registry={{} as FieldErrorProps['registry']}
|
||||
schema={{
|
||||
outputType: SearchOutputType.JSONLogic,
|
||||
}}
|
||||
value={semanticsData[field.key]?.rule ?? {}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
key={field.key}>
|
||||
{editingKey === field.key ? (
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={t('label.name')}
|
||||
name={[field.name, 'name']}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={t('label.description')}
|
||||
name={[field.name, 'description']}>
|
||||
<TextArea />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={t('label.enabled')}
|
||||
name={[field.name, 'enabled']}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
{...field}
|
||||
label={t('label.add-entity', {
|
||||
entity: t('label.rule'),
|
||||
})}
|
||||
name={[field.name, 'rule']}>
|
||||
{/* @ts-expect-error because Form.Item will provide value and onChange */}
|
||||
<QueryBuilderWidget
|
||||
formContext={{
|
||||
entityType: EntityType.TABLE,
|
||||
}}
|
||||
id="rule"
|
||||
label={t('label.rule')}
|
||||
name={`${field.name}.rule`}
|
||||
options={{
|
||||
addButtonText: t('label.add-semantic'),
|
||||
removeButtonText: t('label.remove-semantic'),
|
||||
}}
|
||||
registry={{} as FieldErrorProps['registry']}
|
||||
schema={{
|
||||
outputType: SearchOutputType.JSONLogic,
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col className="d-flex justify-end" span={24}>
|
||||
<Button onClick={() => setEditingKey(null)}>
|
||||
{t('label.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className="m-l-md"
|
||||
type="primary"
|
||||
onClick={() => setEditingKey(null)}>
|
||||
{t('label.save')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
) : (
|
||||
<div className="semantic-rule-editor-view-only">
|
||||
{/* @ts-expect-error because Form.Item will provide value and onChange */}
|
||||
<QueryBuilderWidget
|
||||
readonly
|
||||
formContext={{
|
||||
entityType: EntityType.TABLE,
|
||||
}}
|
||||
registry={{} as FieldErrorProps['registry']}
|
||||
schema={{
|
||||
outputType: SearchOutputType.JSONLogic,
|
||||
}}
|
||||
value={semanticsData[field.key]?.rule ?? {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ExpandableCard>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="d-flex justify-between">
|
||||
<Button
|
||||
className="m-t-md"
|
||||
disabled={!!editingKey}
|
||||
icon={<PlusOutlined />}
|
||||
type="primary"
|
||||
onClick={() =>
|
||||
add({
|
||||
name: '',
|
||||
description: '',
|
||||
rule: '',
|
||||
enabled: false,
|
||||
})
|
||||
}>
|
||||
{t('label.add-entity', {
|
||||
entity: t('label.semantic-plural'),
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
</ExpandableCard>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Card>
|
||||
@ -244,7 +249,7 @@ export const ContractSemanticFormTab: React.FC<{
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={onPrev}>
|
||||
{prevLabel ?? t('label.previous')}
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleNext}>
|
||||
<Button type="primary" onClick={onNext}>
|
||||
{nextLabel ?? t('label.next')}
|
||||
<ArrowRightOutlined />
|
||||
</Button>
|
||||
|
||||
@ -10,6 +10,36 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@import (reference) url('../../../styles/variables.less');
|
||||
|
||||
.add-semantic-button {
|
||||
span {
|
||||
font-weight: 600;
|
||||
}
|
||||
.anticon {
|
||||
font-size: 14px;
|
||||
rotate: 45deg;
|
||||
}
|
||||
}
|
||||
|
||||
.contract-semantic-form-container {
|
||||
.expandable-card {
|
||||
margin-top: 16px;
|
||||
|
||||
.ant-card-head {
|
||||
background: @white !important;
|
||||
border-bottom: 1px solid @grey-200 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-active-card {
|
||||
.ant-card-head {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.semantic-rule-editor-view-only {
|
||||
.ant-divider,
|
||||
.group--conjunctions {
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AxiosError } from 'axios';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DataContractTabMode } from '../../../constants/DataContract.constants';
|
||||
@ -45,27 +46,26 @@ export const ContractTab = () => {
|
||||
]);
|
||||
setContract(contract);
|
||||
} catch {
|
||||
//
|
||||
setContract(undefined);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
const handleDelete = async () => {
|
||||
if (contract?.id) {
|
||||
deleteContractById(contract.id)
|
||||
.then(() => {
|
||||
showSuccessToast(
|
||||
t('message.entity-deleted-successfully', {
|
||||
entity: t('label.contract'),
|
||||
})
|
||||
);
|
||||
fetchContract();
|
||||
setTabMode(DataContractTabMode.VIEW);
|
||||
})
|
||||
.catch((err) => {
|
||||
showErrorToast(err);
|
||||
});
|
||||
try {
|
||||
await deleteContractById(contract.id);
|
||||
showSuccessToast(
|
||||
t('server.entity-deleted-successfully', {
|
||||
entity: t('label.contract'),
|
||||
})
|
||||
);
|
||||
fetchContract();
|
||||
setTabMode(DataContractTabMode.VIEW);
|
||||
} catch (err) {
|
||||
showErrorToast(err as AxiosError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "Der Minimalwert sollte kleiner als der Maximalwert sein.",
|
||||
"minute": "Minute",
|
||||
"missing-config-value": "Fehlender Konfigurationswert: {{field}}",
|
||||
"modified-time-ago-by": "Geändert vor {{time}} von {{by}}",
|
||||
"modify-hierarchy-entity-description": "Ändern Sie die Hierarchie, indem Sie die übergeordnete {{entity}} ändern.",
|
||||
"most-active-users": "Zeigt die aktivsten Benutzer auf der Plattform basierend auf Seitenaufrufen.",
|
||||
"most-expensive-queries-widget-description": "Top 5 der aufwändigsten Abfragen für die Datenvermögenswerte im Dienst. <0>Mehr erfahren.</0>",
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "The minimum value should be smaller than the maximum value.",
|
||||
"minute": "Minute",
|
||||
"missing-config-value": "Missing config value: {{field}}",
|
||||
"modified-time-ago-by": "Modified {{time}} ago by {{by}}",
|
||||
"modify-hierarchy-entity-description": "Modify the hierarchy by changing the Parent {{entity}}.",
|
||||
"most-active-users": "Displays the most active users on the platform based on Page Views.",
|
||||
"most-expensive-queries-widget-description": "Top 5 most expensive queries for the data assets in the service. <0>learn more.</0>",
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "The minimum value should be smaller than the maximum value.",
|
||||
"minute": "Minuto",
|
||||
"missing-config-value": "Falta el valor de configuración: {{field}}",
|
||||
"modified-time-ago-by": "Modificado {{time}} atrás por {{by}}",
|
||||
"modify-hierarchy-entity-description": "Modify the hierarchy by changing the Parent {{entity}}.",
|
||||
"most-active-users": "Muestra los usuarios más activos en la plataforma basado en las vistas de página.",
|
||||
"most-expensive-queries-widget-description": "Las 5 consultas más costosas para los activos de datos en el servicio. <0>saber más.</0>",
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "La vleur minimum value doit être plus petite que la valeur maximum.",
|
||||
"minute": "Minute",
|
||||
"missing-config-value": "Valeur de configuration manquante : {{field}}",
|
||||
"modified-time-ago-by": "Modifié {{time}} par {{by}}",
|
||||
"modify-hierarchy-entity-description": "Modifier la hiérarchie en changeant le {{entity}} parent.",
|
||||
"most-active-users": "Affiche les utilisateurs les plus actifs en fonction du nombre de consultations de pages.",
|
||||
"most-expensive-queries-widget-description": "Top 5 des requêtes les plus coûteuses pour les actifs de données dans le service. <0>en savoir plus.</0>",
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "O valor mínimo debe ser menor que o valor máximo.",
|
||||
"minute": "Minuto",
|
||||
"missing-config-value": "Falta o valor de configuración: {{field}}",
|
||||
"modified-time-ago-by": "Modificado {{time}} por {{by}}",
|
||||
"modify-hierarchy-entity-description": "Modifica a xerarquía cambiando o {{entity}} Pai.",
|
||||
"most-active-users": "Mostra os usuarios máis activos na plataforma baseándose nas Vistas de Páxina.",
|
||||
"most-expensive-queries-widget-description": "As 5 consultas máis custosas para os activos de datos no servizo. <0>saber máis.</0>",
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "The minimum value should be smaller than the maximum value.",
|
||||
"minute": "דקה",
|
||||
"missing-config-value": "חסר ערך הגדרה: {{field}}",
|
||||
"modified-time-ago-by": "עודכן {{time}} לפני {{by}}",
|
||||
"modify-hierarchy-entity-description": "Modify the hierarchy by changing the Parent {{entity}}.",
|
||||
"most-active-users": "מציג את המשתמשים הפעילים ביותר בפלטפורמה על פי צפיות בדף.",
|
||||
"most-expensive-queries-widget-description": "5 השאילתות היקרות ביותר עבור נכסי הנתונים בשירות. <0>למד עוד</0>.",
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "最小値は最大値より小さくする必要があります。",
|
||||
"minute": "分",
|
||||
"missing-config-value": "設定値が不足しています: {{field}}",
|
||||
"modified-time-ago-by": "{{time}} 前に {{by}} によって更新されました",
|
||||
"modify-hierarchy-entity-description": "親{{entity}}を変更して階層を変更します。",
|
||||
"most-active-users": "ページビューに基づく最もアクティブなユーザーの表示。",
|
||||
"most-expensive-queries-widget-description": "サービス内のデータアセットにおける最もコストの高い上位5つのクエリ。<0>詳細を見る。</0>",
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "최소값은 최대값보다 작아야 합니다.",
|
||||
"minute": "분",
|
||||
"missing-config-value": "누락된 구성 값: {{field}}",
|
||||
"modified-time-ago-by": "{{time}} 전에 {{by}}에 의해 수정됨",
|
||||
"modify-hierarchy-entity-description": "상위 {{entity}}를 변경하여 계층 구조를 수정합니다.",
|
||||
"most-active-users": "페이지 조회수를 기반으로 플랫폼에서 가장 활발한 사용자를 표시합니다.",
|
||||
"most-expensive-queries-widget-description": "서비스 내에서 가장 많이 사용된 데이터 자산 상위 5개입니다. <0>자세히 알아보기.</0>",
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "किमान मूल्य कमाल मूल्यापेक्षा लहान असावे.",
|
||||
"minute": "मिनिट",
|
||||
"missing-config-value": "कॉन्फिगरेशन व्हॅल्यू गहाळ आहे: {{field}}",
|
||||
"modified-time-ago-by": "{{time}} पूर्व {{by}} द्वारे संशोधित",
|
||||
"modify-hierarchy-entity-description": "पालक {{entity}} बदलून श्रेणीक्रम बदल करा.",
|
||||
"most-active-users": "पृष्ठ दृश्यांवर आधारित प्लॅटफॉर्मवरील सर्वात सक्रिय वापरकर्ते दर्शवते.",
|
||||
"most-expensive-queries-widget-description": "सेवेतील डेटा मालमत्तांसाठी शीर्ष 5 सर्वात महाग क्वेरी. <0>अधिक जाणून घ्या.</0>",
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "The minimum value should be smaller than the maximum value.",
|
||||
"minute": "Minuut",
|
||||
"missing-config-value": "Ontbrekende configuratiewaarde: {{field}}",
|
||||
"modified-time-ago-by": "Aangepast {{time}} geleden door {{by}}",
|
||||
"modify-hierarchy-entity-description": "Modify the hierarchy by changing the Parent {{entity}}.",
|
||||
"most-active-users": "Toont de meest actieve gebruikers op het platform op basis van paginaweergaven.",
|
||||
"most-expensive-queries-widget-description": "Top 5 meest kostbare queries voor de data-assets in de service. <0>meer informatie.</0>",
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "مقدار حداقل باید کوچکتر از مقدار حداکثر باشد.",
|
||||
"minute": "دقیقه",
|
||||
"missing-config-value": "مقدار پیکربندی گم شده: {{field}}",
|
||||
"modified-time-ago-by": "{{time}} قبل از {{by}} تغییر کرد",
|
||||
"modify-hierarchy-entity-description": "با تغییر والد {{entity}}، سلسلهمراتب را ویرایش کنید.",
|
||||
"most-active-users": "فعالترین کاربران بر اساس بازدیدهای صفحه را نمایش میدهد.",
|
||||
"most-expensive-queries-widget-description": "5 کوئری پرهزینه برتر برای داراییهای داده در سرویس. <0>بیشتر بدانید.</0>",
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "O valor mínimo deve ser menor que o valor máximo.",
|
||||
"minute": "Minuto",
|
||||
"missing-config-value": "Valor de configuração ausente: {{field}}",
|
||||
"modified-time-ago-by": "Modificado {{time}} atrás por {{by}}",
|
||||
"modify-hierarchy-entity-description": "Modifique a hierarquia alterando o Pai {{entity}}.",
|
||||
"most-active-users": "Exibe os usuários mais ativos na plataforma com base nas Visualizações de Página.",
|
||||
"most-expensive-queries-widget-description": "Top 5 consultas mais caras para os ativos de dados no serviço. <0>saiba mais.</0>",
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "The minimum value should be smaller than the maximum value.",
|
||||
"minute": "Minuto",
|
||||
"missing-config-value": "Valor de configuração em falta: {{field}}",
|
||||
"modified-time-ago-by": "Modificado {{time}} atrás por {{by}}",
|
||||
"modify-hierarchy-entity-description": "Modify the hierarchy by changing the Parent {{entity}}.",
|
||||
"most-active-users": "Exibe os Utilizadores mais ativos na plataforma com base nas Visualizações de Página.",
|
||||
"most-expensive-queries-widget-description": "Top 5 consultas mais dispendiosas para os ativos de dados no serviço. <0>saber mais.</0>",
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "Минимальное значение должно быть меньше максимального значения.",
|
||||
"minute": "Минута",
|
||||
"missing-config-value": "Отсутствует значение конфигурации: {{field}}",
|
||||
"modified-time-ago-by": "Изменено {{time}} назад {{by}}",
|
||||
"modify-hierarchy-entity-description": "Измените иерархию, изменив родительский элемент {{entity}}.",
|
||||
"most-active-users": "Отображает самых активных пользователей на платформе на основе просмотров страниц.",
|
||||
"most-expensive-queries-widget-description": "Топ-5 самых дорогих запросов для объектов данных в сервисе. <0>узнать больше.</0>",
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "The minimum value should be smaller than the maximum value.",
|
||||
"minute": "Minute",
|
||||
"missing-config-value": "ค่าการกำหนดค่าที่หายไป: {{field}}",
|
||||
"modified-time-ago-by": "แก้ไข {{time}} ก่อนหน้า {{by}}",
|
||||
"modify-hierarchy-entity-description": "Modify the hierarchy by changing the Parent {{entity}}.",
|
||||
"most-active-users": "Displays the most active users on the platform based on Page Views.",
|
||||
"most-expensive-queries-widget-description": "5 อันดับคิวรีที่ใช้ทรัพยากรมากที่สุดสำหรับสินทรัพย์ข้อมูลในบริการ <0>เรียนรู้เพิ่มเติม</0>",
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "Minimum değer maksimum değerden küçük olmalıdır.",
|
||||
"minute": "Dakika",
|
||||
"missing-config-value": "Eksik yapılandırma değeri: {{field}}",
|
||||
"modified-time-ago-by": "{{time}} önce {{by}} tarafından değiştirildi",
|
||||
"modify-hierarchy-entity-description": "Üst {{entity}} değiştirerek hiyerarşiyi değiştirin.",
|
||||
"most-active-users": "Sayfa Görüntülemelerine göre platformdaki en aktif kullanıcıları görüntüler.",
|
||||
"most-expensive-queries-widget-description": "Servisteki veri varlıkları için en maliyetli ilk 5 sorgu. <0>daha fazla bilgi edinin.</0>",
|
||||
|
||||
@ -2085,6 +2085,7 @@
|
||||
"minimum-value-error": "最小值应小于最大值",
|
||||
"minute": "分钟",
|
||||
"missing-config-value": "缺少配置值:{{field}}",
|
||||
"modified-time-ago-by": "{{time}} 前由 {{by}} 修改",
|
||||
"modify-hierarchy-entity-description": "通过更改父{{entity}}来修改层级结构.",
|
||||
"most-active-users": "显示平台上最活跃的用户(基于页面浏览量)",
|
||||
"most-expensive-queries-widget-description": "服务中数据资产的前 5 个最昂贵查询。<0>了解更多。</0>",
|
||||
|
||||
@ -115,8 +115,8 @@ export const getTestCaseSummaryChartItems = (testCaseSummary?: TestSummary) => {
|
||||
color: GREEN_3,
|
||||
chartData: [
|
||||
{ name: 'success', value: success, color: GREEN_3 },
|
||||
{ name: 'aborted', value: failed, color: YELLOW_2 },
|
||||
{ name: 'failed', value: aborted, color: RED_3 },
|
||||
{ name: 'failed', value: failed, color: RED_3 },
|
||||
{ name: 'aborted', value: aborted, color: YELLOW_2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import Icon from '@ant-design/icons';
|
||||
import { isArray, isNil, isUndefined, omit, omitBy } from 'lodash';
|
||||
import { ReactComponent as AccuracyIcon } from '../../assets/svg/ic-accuracy.svg';
|
||||
import { ReactComponent as CompletenessIcon } from '../../assets/svg/ic-completeness.svg';
|
||||
@ -21,14 +20,10 @@ import { ReactComponent as UniquenessIcon } from '../../assets/svg/ic-uniqueness
|
||||
import { ReactComponent as ValidityIcon } from '../../assets/svg/ic-validity.svg';
|
||||
import { ReactComponent as NoDimensionIcon } from '../../assets/svg/no-dimension-icon.svg';
|
||||
import { TestCaseSearchParams } from '../../components/DataQuality/DataQuality.interface';
|
||||
import { TEST_CASE_STATUS_ICON } from '../../constants/DataQuality.constants';
|
||||
import { TEST_CASE_FILTERS } from '../../constants/profiler.constant';
|
||||
import { Table } from '../../generated/entity/data/table';
|
||||
import { DataQualityReport } from '../../generated/tests/dataQualityReport';
|
||||
import {
|
||||
TestCase,
|
||||
TestCaseParameterValue,
|
||||
} from '../../generated/tests/testCase';
|
||||
import { TestCaseParameterValue } from '../../generated/tests/testCase';
|
||||
import {
|
||||
DataQualityDimensions,
|
||||
TestDataType,
|
||||
@ -327,15 +322,3 @@ export const convertSearchSourceToTable = (
|
||||
...searchSource,
|
||||
columns: searchSource.columns || [],
|
||||
} as Table);
|
||||
|
||||
export const getTestCaseStatusIcon = (record: TestCase) => (
|
||||
<Icon
|
||||
className="test-status-icon"
|
||||
component={
|
||||
TEST_CASE_STATUS_ICON[
|
||||
(record?.testCaseResult?.testCaseStatus ??
|
||||
'Queued') as keyof typeof TEST_CASE_STATUS_ICON
|
||||
]
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user