mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-06 14:26:28 +00:00
modify the rule for dataAssetRule and semantic form contract (#22744)
* modify the rule for dataAssetRule and semantic form contract * fix the query input placement * fix the rule config * update logic to add specific fields * update json logic to get fields * address comments * fix tier option logic * fix delete data contract test --------- Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Co-authored-by: Pranita <pfulsundar8@gmail.com>
This commit is contained in:
parent
2d58db69d2
commit
78839892b6
@ -414,6 +414,19 @@ test.describe('Data Contracts', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await page.getByTestId('delete-contract-button').click();
|
await page.getByTestId('delete-contract-button').click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.locator('.ant-modal-title')
|
||||||
|
.getByText(`Delete dataContract "${DATA_CONTRACT_DETAILS.name}"`)
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId('confirmation-text-input').click();
|
||||||
|
await page.getByTestId('confirmation-text-input').fill('DELETE');
|
||||||
|
|
||||||
|
await expect(page.getByTestId('confirm-button')).toBeEnabled();
|
||||||
|
|
||||||
|
await page.getByTestId('confirm-button').click();
|
||||||
await deleteContractResponse;
|
await deleteContractResponse;
|
||||||
|
|
||||||
await toastNotification(page, '"Contract" deleted successfully!');
|
await toastNotification(page, '"Contract" deleted successfully!');
|
||||||
|
@ -31,6 +31,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { ReactComponent as AddPlaceHolderIcon } from '../../assets/svg/add-placeholder.svg';
|
import { ReactComponent as AddPlaceHolderIcon } from '../../assets/svg/add-placeholder.svg';
|
||||||
import { ReactComponent as IconEdit } from '../../assets/svg/edit-new.svg';
|
import { ReactComponent as IconEdit } from '../../assets/svg/edit-new.svg';
|
||||||
import { ReactComponent as IconDelete } from '../../assets/svg/ic-delete.svg';
|
import { ReactComponent as IconDelete } from '../../assets/svg/ic-delete.svg';
|
||||||
|
import { EntityReferenceFields } from '../../enums/AdvancedSearch.enum';
|
||||||
import { SIZE } from '../../enums/common.enum';
|
import { SIZE } from '../../enums/common.enum';
|
||||||
import {
|
import {
|
||||||
ProviderType,
|
ProviderType,
|
||||||
@ -43,6 +44,7 @@ import {
|
|||||||
updateSettingsConfig,
|
updateSettingsConfig,
|
||||||
} from '../../rest/settingConfigAPI';
|
} from '../../rest/settingConfigAPI';
|
||||||
import i18n, { t } from '../../utils/i18next/LocalUtil';
|
import i18n, { t } from '../../utils/i18next/LocalUtil';
|
||||||
|
import jsonLogicSearchClassBase from '../../utils/JSONLogicSearchClassBase';
|
||||||
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||||
import QueryBuilderWidget from '../common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget';
|
import QueryBuilderWidget from '../common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget';
|
||||||
import RichTextEditorPreviewerNew from '../common/RichTextEditor/RichTextEditorPreviewNew';
|
import RichTextEditorPreviewerNew from '../common/RichTextEditor/RichTextEditorPreviewNew';
|
||||||
@ -134,6 +136,18 @@ export const SemanticsRuleForm: React.FC<{
|
|||||||
form.setFieldsValue(semanticsRule);
|
form.setFieldsValue(semanticsRule);
|
||||||
}, [semanticsRule]);
|
}, [semanticsRule]);
|
||||||
|
|
||||||
|
const queryBuilderFields = useMemo(() => {
|
||||||
|
const fields = jsonLogicSearchClassBase.getMapFields();
|
||||||
|
|
||||||
|
return {
|
||||||
|
[EntityReferenceFields.TAG]: fields[EntityReferenceFields.TAG],
|
||||||
|
[EntityReferenceFields.TIER]: fields[EntityReferenceFields.TIER],
|
||||||
|
[EntityReferenceFields.DOMAIN]: fields[EntityReferenceFields.DOMAIN],
|
||||||
|
[EntityReferenceFields.DATA_PRODUCT]:
|
||||||
|
fields[EntityReferenceFields.DATA_PRODUCT],
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@ -167,7 +181,6 @@ export const SemanticsRuleForm: React.FC<{
|
|||||||
<Input.TextArea placeholder={t('label.description')} rows={2} />
|
<Input.TextArea placeholder={t('label.description')} rows={2} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('label.rule')}
|
|
||||||
name="rule"
|
name="rule"
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
@ -176,9 +189,13 @@ export const SemanticsRuleForm: React.FC<{
|
|||||||
]}>
|
]}>
|
||||||
{/* @ts-expect-error because Form.Item will provide value and onChange */}
|
{/* @ts-expect-error because Form.Item will provide value and onChange */}
|
||||||
<QueryBuilderWidget
|
<QueryBuilderWidget
|
||||||
|
defaultField={EntityReferenceFields.TAG}
|
||||||
|
fields={queryBuilderFields}
|
||||||
|
label={t('label.rule')}
|
||||||
schema={{
|
schema={{
|
||||||
outputType: SearchOutputType.JSONLogic,
|
outputType: SearchOutputType.JSONLogic,
|
||||||
}}
|
}}
|
||||||
|
subField="tagFQN"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -223,6 +223,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ant-form-item-control-input-content {
|
.ant-form-item-control-input-content {
|
||||||
|
.group--field {
|
||||||
|
align-self: baseline;
|
||||||
|
}
|
||||||
input[type='text'] {
|
input[type='text'] {
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
color: @grey-700;
|
color: @grey-700;
|
||||||
@ -239,7 +242,7 @@
|
|||||||
border: 1px solid @grey-300;
|
border: 1px solid @grey-300;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
color: @grey-700;
|
color: @grey-700;
|
||||||
height: 40px;
|
min-height: 40px;
|
||||||
box-shadow: 0 1px 2px 0px @grey-27;
|
box-shadow: 0 1px 2px 0px @grey-27;
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
@ -318,9 +321,18 @@
|
|||||||
height: 40px !important;
|
height: 40px !important;
|
||||||
box-shadow: 0 1px 2px 0px @grey-27;
|
box-shadow: 0 1px 2px 0px @grey-27;
|
||||||
|
|
||||||
&:focus,
|
.ant-select-selector,
|
||||||
&:hover {
|
.ant-mentions {
|
||||||
border-color: @primary-color;
|
border: 1px solid @grey-300;
|
||||||
|
padding: 4px 12px !important;
|
||||||
|
color: @grey-700;
|
||||||
|
min-height: 40px !important;
|
||||||
|
box-shadow: 0 1px 2px 0px @grey-27;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
border-color: @primary-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,5 +15,5 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
@ -19,14 +19,16 @@ import Card from 'antd/lib/card/Card';
|
|||||||
import TextArea from 'antd/lib/input/TextArea';
|
import TextArea from 'antd/lib/input/TextArea';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { isNull } from 'lodash';
|
import { isNull } from 'lodash';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ReactComponent as DeleteIcon } from '../../../assets/svg/ic-trash.svg';
|
import { ReactComponent as DeleteIcon } from '../../../assets/svg/ic-trash.svg';
|
||||||
import { ReactComponent as LeftOutlined } from '../../../assets/svg/left-arrow.svg';
|
import { ReactComponent as LeftOutlined } from '../../../assets/svg/left-arrow.svg';
|
||||||
import { ReactComponent as RightIcon } from '../../../assets/svg/right-arrow.svg';
|
import { ReactComponent as RightIcon } from '../../../assets/svg/right-arrow.svg';
|
||||||
import { ReactComponent as PlusIcon } from '../../../assets/svg/x-colored.svg';
|
import { ReactComponent as PlusIcon } from '../../../assets/svg/x-colored.svg';
|
||||||
|
import { EntityReferenceFields } from '../../../enums/AdvancedSearch.enum';
|
||||||
import { EntityType } from '../../../enums/entity.enum';
|
import { EntityType } from '../../../enums/entity.enum';
|
||||||
import { DataContract } from '../../../generated/entity/data/dataContract';
|
import { DataContract } from '../../../generated/entity/data/dataContract';
|
||||||
|
import jsonLogicSearchClassBase from '../../../utils/JSONLogicSearchClassBase';
|
||||||
import ExpandableCard from '../../common/ExpandableCard/ExpandableCard';
|
import ExpandableCard from '../../common/ExpandableCard/ExpandableCard';
|
||||||
import QueryBuilderWidget from '../../common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget';
|
import QueryBuilderWidget from '../../common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget';
|
||||||
import { EditIconButton } from '../../common/IconButtons/EditIconButton';
|
import { EditIconButton } from '../../common/IconButtons/EditIconButton';
|
||||||
@ -105,6 +107,15 @@ export const ContractSemanticFormTab: React.FC<{
|
|||||||
setEditingKey(0);
|
setEditingKey(0);
|
||||||
}, [initialValues]);
|
}, [initialValues]);
|
||||||
|
|
||||||
|
// Remove extension field from common config
|
||||||
|
const queryBuilderFields = useMemo(() => {
|
||||||
|
const fields = jsonLogicSearchClassBase.getCommonConfig();
|
||||||
|
|
||||||
|
delete fields[EntityReferenceFields.EXTENSION];
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className="contract-semantic-form-container container bg-grey p-box">
|
<Card className="contract-semantic-form-container container bg-grey p-box">
|
||||||
@ -253,6 +264,7 @@ export const ContractSemanticFormTab: React.FC<{
|
|||||||
name={[field.name, 'rule']}>
|
name={[field.name, 'rule']}>
|
||||||
{/* @ts-expect-error because Form.Item will provide value and onChange */}
|
{/* @ts-expect-error because Form.Item will provide value and onChange */}
|
||||||
<QueryBuilderWidget
|
<QueryBuilderWidget
|
||||||
|
fields={queryBuilderFields}
|
||||||
formContext={{
|
formContext={{
|
||||||
entityType: EntityType.TABLE,
|
entityType: EntityType.TABLE,
|
||||||
}}
|
}}
|
||||||
@ -299,6 +311,7 @@ export const ContractSemanticFormTab: React.FC<{
|
|||||||
{/* @ts-expect-error because Form.Item will provide value and onChange */}
|
{/* @ts-expect-error because Form.Item will provide value and onChange */}
|
||||||
<QueryBuilderWidget
|
<QueryBuilderWidget
|
||||||
readonly
|
readonly
|
||||||
|
fields={queryBuilderFields}
|
||||||
formContext={{
|
formContext={{
|
||||||
entityType: EntityType.TABLE,
|
entityType: EntityType.TABLE,
|
||||||
}}
|
}}
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
getContractByEntityId,
|
getContractByEntityId,
|
||||||
} from '../../../rest/contractAPI';
|
} from '../../../rest/contractAPI';
|
||||||
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
|
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
|
||||||
|
import DeleteWidgetModal from '../../common/DeleteWidget/DeleteWidgetModal';
|
||||||
import Loader from '../../common/Loader/Loader';
|
import Loader from '../../common/Loader/Loader';
|
||||||
import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider';
|
import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider';
|
||||||
import AddDataContract from '../AddDataContract/AddDataContract';
|
import AddDataContract from '../AddDataContract/AddDataContract';
|
||||||
@ -38,6 +39,7 @@ export const ContractTab = () => {
|
|||||||
);
|
);
|
||||||
const [contract, setContract] = useState<DataContract>();
|
const [contract, setContract] = useState<DataContract>();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
|
||||||
|
|
||||||
const fetchContract = async () => {
|
const fetchContract = async () => {
|
||||||
try {
|
try {
|
||||||
@ -55,21 +57,30 @@ export const ContractTab = () => {
|
|||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
if (contract?.id) {
|
if (contract?.id) {
|
||||||
try {
|
setIsDeleteModalVisible(true);
|
||||||
await deleteContractById(contract.id);
|
|
||||||
showSuccessToast(
|
|
||||||
t('server.entity-deleted-successfully', {
|
|
||||||
entity: t('label.contract'),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
fetchContract();
|
|
||||||
setTabMode(DataContractTabMode.VIEW);
|
|
||||||
} catch (err) {
|
|
||||||
showErrorToast(err as AxiosError);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleContractDeleteConfirm = async () => {
|
||||||
|
if (!contract?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await deleteContractById(contract.id);
|
||||||
|
showSuccessToast(
|
||||||
|
t('server.entity-deleted-successfully', {
|
||||||
|
entity: t('label.contract'),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
fetchContract();
|
||||||
|
setTabMode(DataContractTabMode.VIEW);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error as AxiosError);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDeleteModalVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchContract();
|
fetchContract();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
@ -122,6 +133,18 @@ export const ContractTab = () => {
|
|||||||
return isLoading ? (
|
return isLoading ? (
|
||||||
<Loader />
|
<Loader />
|
||||||
) : (
|
) : (
|
||||||
<div className="contract-tab-container">{content}</div>
|
<div className="contract-tab-container">
|
||||||
|
{content}
|
||||||
|
<DeleteWidgetModal
|
||||||
|
allowSoftDelete={false}
|
||||||
|
entityName={contract?.name ?? ''}
|
||||||
|
entityType={EntityType.DATA_CONTRACT}
|
||||||
|
visible={isDeleteModalVisible}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsDeleteModalVisible(false);
|
||||||
|
}}
|
||||||
|
onDelete={handleContractDeleteConfirm}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -60,12 +60,13 @@ import { useAdvanceSearch } from '../../../../../Explore/AdvanceSearchProvider/A
|
|||||||
import { SearchOutputType } from '../../../../../Explore/AdvanceSearchProvider/AdvanceSearchProvider.interface';
|
import { SearchOutputType } from '../../../../../Explore/AdvanceSearchProvider/AdvanceSearchProvider.interface';
|
||||||
import './query-builder-widget.less';
|
import './query-builder-widget.less';
|
||||||
|
|
||||||
const QueryBuilderWidget: FC<WidgetProps> = ({
|
const QueryBuilderWidget: FC<
|
||||||
onChange,
|
WidgetProps & {
|
||||||
schema,
|
fields?: Config['fields'];
|
||||||
value,
|
defaultField?: string;
|
||||||
...props
|
subField?: string;
|
||||||
}) => {
|
}
|
||||||
|
> = ({ onChange, schema, value, fields, defaultField, subField, ...props }) => {
|
||||||
const {
|
const {
|
||||||
config,
|
config,
|
||||||
treeInternal,
|
treeInternal,
|
||||||
@ -202,7 +203,7 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
|
|||||||
} else {
|
} else {
|
||||||
const emptyJsonTree =
|
const emptyJsonTree =
|
||||||
outputType === SearchOutputType.JSONLogic
|
outputType === SearchOutputType.JSONLogic
|
||||||
? getEmptyJsonTreeForQueryBuilder()
|
? getEmptyJsonTreeForQueryBuilder(defaultField, subField)
|
||||||
: getEmptyJsonTree();
|
: getEmptyJsonTree();
|
||||||
|
|
||||||
const tree = QbUtils.loadTree(emptyJsonTree);
|
const tree = QbUtils.loadTree(emptyJsonTree);
|
||||||
@ -253,6 +254,7 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
|
|||||||
)}
|
)}
|
||||||
<Query
|
<Query
|
||||||
{...config}
|
{...config}
|
||||||
|
fields={fields ?? config.fields}
|
||||||
renderBuilder={(props) => {
|
renderBuilder={(props) => {
|
||||||
// Store the actions for external access
|
// Store the actions for external access
|
||||||
if (!queryActions) {
|
if (!queryActions) {
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { EntityReferenceFields } from '../enums/AdvancedSearch.enum';
|
||||||
|
|
||||||
export enum DataContractMode {
|
export enum DataContractMode {
|
||||||
YAML,
|
YAML,
|
||||||
UI,
|
UI,
|
||||||
@ -28,3 +30,14 @@ export enum EDataContractTab {
|
|||||||
SEMANTICS,
|
SEMANTICS,
|
||||||
QUALITY,
|
QUALITY,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DATA_ASSET_RULE_FIELDS_NOT_TO_RENDER = [
|
||||||
|
EntityReferenceFields.EXTENSION,
|
||||||
|
EntityReferenceFields.OWNERS,
|
||||||
|
EntityReferenceFields.NAME,
|
||||||
|
EntityReferenceFields.DESCRIPTION,
|
||||||
|
EntityReferenceFields.TIER,
|
||||||
|
EntityReferenceFields.SERVICE,
|
||||||
|
EntityReferenceFields.DISPLAY_NAME,
|
||||||
|
EntityReferenceFields.DELETED,
|
||||||
|
];
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Collate.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import { EntityReferenceFields } from '../enums/AdvancedSearch.enum';
|
||||||
|
|
||||||
|
export const GLOSSARY_ENTITY_FIELDS_KEYS: EntityReferenceFields[] = [
|
||||||
|
EntityReferenceFields.REVIEWERS,
|
||||||
|
EntityReferenceFields.UPDATED_BY,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TABLE_ENTITY_FIELDS_KEYS: EntityReferenceFields[] = [
|
||||||
|
EntityReferenceFields.DATABASE,
|
||||||
|
EntityReferenceFields.DATABASE_SCHEMA,
|
||||||
|
EntityReferenceFields.TABLE_TYPE,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const COMMON_ENTITY_FIELDS_KEYS: EntityReferenceFields[] = [
|
||||||
|
EntityReferenceFields.SERVICE,
|
||||||
|
EntityReferenceFields.OWNERS,
|
||||||
|
EntityReferenceFields.DISPLAY_NAME,
|
||||||
|
EntityReferenceFields.NAME,
|
||||||
|
EntityReferenceFields.DESCRIPTION,
|
||||||
|
EntityReferenceFields.TAG,
|
||||||
|
EntityReferenceFields.DOMAIN,
|
||||||
|
EntityReferenceFields.DATA_PRODUCT,
|
||||||
|
EntityReferenceFields.TIER,
|
||||||
|
EntityReferenceFields.EXTENSION,
|
||||||
|
];
|
@ -96,9 +96,12 @@ export enum EntityReferenceFields {
|
|||||||
DISPLAY_NAME = 'displayName',
|
DISPLAY_NAME = 'displayName',
|
||||||
TAG = 'tags',
|
TAG = 'tags',
|
||||||
TIER = 'tier.tagFQN',
|
TIER = 'tier.tagFQN',
|
||||||
|
DOMAIN = 'domain',
|
||||||
|
DATA_PRODUCT = 'dataProduct',
|
||||||
TABLE_TYPE = 'tableType',
|
TABLE_TYPE = 'tableType',
|
||||||
EXTENSION = 'extension',
|
EXTENSION = 'extension',
|
||||||
SERVICE = 'service.name',
|
SERVICE = 'service.name',
|
||||||
UPDATED_BY = 'updatedBy',
|
UPDATED_BY = 'updatedBy',
|
||||||
CHANGE_DESCRIPTION = 'changeDescription',
|
CHANGE_DESCRIPTION = 'changeDescription',
|
||||||
|
DELETED = 'deleted',
|
||||||
}
|
}
|
||||||
|
@ -454,7 +454,8 @@ export const getEmptyJsonTree = (
|
|||||||
* This structure allows easy addition of groups and rules
|
* This structure allows easy addition of groups and rules
|
||||||
*/
|
*/
|
||||||
export const getEmptyJsonTreeForQueryBuilder = (
|
export const getEmptyJsonTreeForQueryBuilder = (
|
||||||
defaultField: string = EntityReferenceFields.OWNERS
|
defaultField: string = EntityReferenceFields.OWNERS,
|
||||||
|
subField = 'fullyQualifiedName'
|
||||||
): OldJsonTree => {
|
): OldJsonTree => {
|
||||||
const uuid1 = QbUtils.uuid();
|
const uuid1 = QbUtils.uuid();
|
||||||
const uuid2 = QbUtils.uuid();
|
const uuid2 = QbUtils.uuid();
|
||||||
@ -483,7 +484,7 @@ export const getEmptyJsonTreeForQueryBuilder = (
|
|||||||
type: 'rule',
|
type: 'rule',
|
||||||
id: uuid3,
|
id: uuid3,
|
||||||
properties: {
|
properties: {
|
||||||
field: 'owners.fullyQualifiedName',
|
field: `${defaultField}.${subField}`,
|
||||||
operator: 'select_equals',
|
operator: 'select_equals',
|
||||||
value: [],
|
value: [],
|
||||||
valueSrc: ['value'],
|
valueSrc: ['value'],
|
||||||
|
@ -12,24 +12,36 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
AntdConfig,
|
AntdConfig,
|
||||||
|
AsyncFetchListValuesResult,
|
||||||
Config,
|
Config,
|
||||||
|
FieldOrGroup,
|
||||||
Fields,
|
Fields,
|
||||||
|
ListItem,
|
||||||
ListValues,
|
ListValues,
|
||||||
Operators,
|
Operators,
|
||||||
SelectFieldSettings,
|
SelectFieldSettings,
|
||||||
} from '@react-awesome-query-builder/antd';
|
} from '@react-awesome-query-builder/antd';
|
||||||
import { get, sortBy } from 'lodash';
|
import { get, sortBy, toLower } from 'lodash';
|
||||||
import { TEXT_FIELD_OPERATORS } from '../constants/AdvancedSearch.constants';
|
import { TEXT_FIELD_OPERATORS } from '../constants/AdvancedSearch.constants';
|
||||||
import { PAGE_SIZE_BASE } from '../constants/constants';
|
import { PAGE_SIZE_BASE } from '../constants/constants';
|
||||||
|
import {
|
||||||
|
COMMON_ENTITY_FIELDS_KEYS,
|
||||||
|
GLOSSARY_ENTITY_FIELDS_KEYS,
|
||||||
|
TABLE_ENTITY_FIELDS_KEYS,
|
||||||
|
} from '../constants/JSONLogicSearch.constants';
|
||||||
import {
|
import {
|
||||||
EntityFields,
|
EntityFields,
|
||||||
EntityReferenceFields,
|
EntityReferenceFields,
|
||||||
} from '../enums/AdvancedSearch.enum';
|
} from '../enums/AdvancedSearch.enum';
|
||||||
import { SearchIndex } from '../enums/search.enum';
|
import { SearchIndex } from '../enums/search.enum';
|
||||||
import { searchData } from '../rest/miscAPI';
|
import { searchData } from '../rest/miscAPI';
|
||||||
|
import { getTags } from '../rest/tagAPI';
|
||||||
import advancedSearchClassBase from './AdvancedSearchClassBase';
|
import advancedSearchClassBase from './AdvancedSearchClassBase';
|
||||||
import { t } from './i18next/LocalUtil';
|
import { t } from './i18next/LocalUtil';
|
||||||
import { renderJSONLogicQueryBuilderButtons } from './QueryBuilderUtils';
|
import {
|
||||||
|
getFieldsByKeys,
|
||||||
|
renderJSONLogicQueryBuilderButtons,
|
||||||
|
} from './QueryBuilderUtils';
|
||||||
|
|
||||||
class JSONLogicSearchClassBase {
|
class JSONLogicSearchClassBase {
|
||||||
baseConfig = AntdConfig as Config;
|
baseConfig = AntdConfig as Config;
|
||||||
@ -143,6 +155,8 @@ class JSONLogicSearchClassBase {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mapFields: Record<string, FieldOrGroup>;
|
||||||
|
|
||||||
defaultSelectOperators = [
|
defaultSelectOperators = [
|
||||||
'select_equals',
|
'select_equals',
|
||||||
'select_not_equals',
|
'select_not_equals',
|
||||||
@ -152,135 +166,8 @@ class JSONLogicSearchClassBase {
|
|||||||
'is_not_null',
|
'is_not_null',
|
||||||
];
|
];
|
||||||
|
|
||||||
public searchAutocomplete: (args: {
|
constructor() {
|
||||||
searchIndex: SearchIndex | SearchIndex[];
|
this.mapFields = {
|
||||||
fieldName: string;
|
|
||||||
fieldLabel: string;
|
|
||||||
}) => SelectFieldSettings['asyncFetch'] = ({
|
|
||||||
searchIndex,
|
|
||||||
fieldName,
|
|
||||||
fieldLabel,
|
|
||||||
}) => {
|
|
||||||
return (search) => {
|
|
||||||
return searchData(
|
|
||||||
Array.isArray(search) ? search.join(',') : search ?? '',
|
|
||||||
1,
|
|
||||||
PAGE_SIZE_BASE,
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
searchIndex ?? SearchIndex.DATA_ASSET,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
).then((response) => {
|
|
||||||
const data = response.data.hits.hits;
|
|
||||||
|
|
||||||
return {
|
|
||||||
values: data.map((item) => ({
|
|
||||||
value: get(item._source, fieldName, ''),
|
|
||||||
title: get(item._source, fieldLabel, ''),
|
|
||||||
})),
|
|
||||||
hasMore: false,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
mainWidgetProps = {
|
|
||||||
fullWidth: true,
|
|
||||||
valueLabel: t('label.criteria') + ':',
|
|
||||||
};
|
|
||||||
|
|
||||||
glossaryEntityFields: Fields = {
|
|
||||||
[EntityReferenceFields.REVIEWERS]: {
|
|
||||||
label: t('label.reviewer-plural'),
|
|
||||||
type: '!group',
|
|
||||||
mode: 'some',
|
|
||||||
defaultField: 'fullyQualifiedName',
|
|
||||||
subfields: {
|
|
||||||
fullyQualifiedName: {
|
|
||||||
label: 'Reviewers New',
|
|
||||||
type: 'select',
|
|
||||||
mainWidgetProps: this.mainWidgetProps,
|
|
||||||
operators: this.defaultSelectOperators,
|
|
||||||
fieldSettings: {
|
|
||||||
asyncFetch: advancedSearchClassBase.autocomplete({
|
|
||||||
searchIndex: [SearchIndex.USER, SearchIndex.TEAM],
|
|
||||||
entityField: EntityFields.DISPLAY_NAME_KEYWORD,
|
|
||||||
}),
|
|
||||||
useAsyncSearch: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[EntityReferenceFields.UPDATED_BY]: {
|
|
||||||
label: t('label.updated-by'),
|
|
||||||
type: 'select',
|
|
||||||
mainWidgetProps: this.mainWidgetProps,
|
|
||||||
operators: [...this.defaultSelectOperators, 'isOwner', 'isReviewer'],
|
|
||||||
fieldSettings: {
|
|
||||||
asyncFetch: advancedSearchClassBase.autocomplete({
|
|
||||||
searchIndex: [SearchIndex.USER, SearchIndex.TEAM],
|
|
||||||
entityField: EntityFields.DISPLAY_NAME_KEYWORD,
|
|
||||||
}),
|
|
||||||
useAsyncSearch: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
tableEntityFields: Fields = {
|
|
||||||
[EntityReferenceFields.DATABASE]: {
|
|
||||||
label: t('label.database'),
|
|
||||||
type: 'select',
|
|
||||||
mainWidgetProps: this.mainWidgetProps,
|
|
||||||
operators: this.defaultSelectOperators,
|
|
||||||
fieldSettings: {
|
|
||||||
asyncFetch: advancedSearchClassBase.autocomplete({
|
|
||||||
searchIndex: SearchIndex.TABLE,
|
|
||||||
entityField: EntityFields.DATABASE_NAME,
|
|
||||||
isCaseInsensitive: true,
|
|
||||||
}),
|
|
||||||
useAsyncSearch: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
[EntityReferenceFields.DATABASE_SCHEMA]: {
|
|
||||||
label: t('label.database-schema'),
|
|
||||||
type: 'select',
|
|
||||||
mainWidgetProps: this.mainWidgetProps,
|
|
||||||
operators: this.defaultSelectOperators,
|
|
||||||
fieldSettings: {
|
|
||||||
asyncFetch: advancedSearchClassBase.autocomplete({
|
|
||||||
searchIndex: SearchIndex.TABLE,
|
|
||||||
entityField: EntityFields.DATABASE_SCHEMA_NAME,
|
|
||||||
isCaseInsensitive: true,
|
|
||||||
}),
|
|
||||||
useAsyncSearch: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
[EntityReferenceFields.TABLE_TYPE]: {
|
|
||||||
label: t('label.table-type'),
|
|
||||||
type: 'select',
|
|
||||||
mainWidgetProps: this.mainWidgetProps,
|
|
||||||
operators: this.defaultSelectOperators,
|
|
||||||
fieldSettings: {
|
|
||||||
asyncFetch: advancedSearchClassBase.autocomplete({
|
|
||||||
searchIndex: SearchIndex.TABLE,
|
|
||||||
entityField: EntityFields.TABLE_TYPE,
|
|
||||||
}),
|
|
||||||
useAsyncSearch: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
public getCommonConfig = (_: {
|
|
||||||
entitySearchIndex?: Array<SearchIndex>;
|
|
||||||
tierOptions?: Promise<ListValues>;
|
|
||||||
}) => {
|
|
||||||
return {
|
|
||||||
[EntityReferenceFields.SERVICE]: {
|
[EntityReferenceFields.SERVICE]: {
|
||||||
label: t('label.service'),
|
label: t('label.service'),
|
||||||
type: 'select',
|
type: 'select',
|
||||||
@ -375,12 +262,229 @@ class JSONLogicSearchClassBase {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[EntityReferenceFields.DOMAIN]: {
|
||||||
|
label: t('label.domain'),
|
||||||
|
type: '!group',
|
||||||
|
mode: 'some',
|
||||||
|
defaultField: 'fullyQualifiedName',
|
||||||
|
subfields: {
|
||||||
|
fullyQualifiedName: {
|
||||||
|
label: 'Domain',
|
||||||
|
type: 'select',
|
||||||
|
mainWidgetProps: this.mainWidgetProps,
|
||||||
|
operators: this.defaultSelectOperators,
|
||||||
|
fieldSettings: {
|
||||||
|
asyncFetch: this.searchAutocomplete({
|
||||||
|
searchIndex: SearchIndex.DOMAIN,
|
||||||
|
fieldName: 'fullyQualifiedName',
|
||||||
|
fieldLabel: 'name',
|
||||||
|
}),
|
||||||
|
useAsyncSearch: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[EntityReferenceFields.DATA_PRODUCT]: {
|
||||||
|
label: t('label.data-product'),
|
||||||
|
type: '!group',
|
||||||
|
mode: 'some',
|
||||||
|
defaultField: 'fullyQualifiedName',
|
||||||
|
subfields: {
|
||||||
|
fullyQualifiedName: {
|
||||||
|
label: 'Data Product',
|
||||||
|
type: 'select',
|
||||||
|
mainWidgetProps: this.mainWidgetProps,
|
||||||
|
operators: this.defaultSelectOperators,
|
||||||
|
fieldSettings: {
|
||||||
|
asyncFetch: this.searchAutocomplete({
|
||||||
|
searchIndex: SearchIndex.DATA_PRODUCT,
|
||||||
|
fieldName: 'fullyQualifiedName',
|
||||||
|
fieldLabel: 'name',
|
||||||
|
}),
|
||||||
|
useAsyncSearch: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[EntityReferenceFields.TIER]: {
|
||||||
|
label: t('label.tier'),
|
||||||
|
type: 'select',
|
||||||
|
mainWidgetProps: this.mainWidgetProps,
|
||||||
|
operators: this.defaultSelectOperators,
|
||||||
|
fieldSettings: {
|
||||||
|
asyncFetch: this.autoCompleteTier,
|
||||||
|
useAsyncSearch: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
[EntityReferenceFields.EXTENSION]: {
|
[EntityReferenceFields.EXTENSION]: {
|
||||||
label: t('label.custom-property-plural'),
|
label: t('label.custom-property-plural'),
|
||||||
type: '!struct',
|
type: '!struct',
|
||||||
mainWidgetProps: this.mainWidgetProps,
|
mainWidgetProps: this.mainWidgetProps,
|
||||||
subfields: {},
|
subfields: {},
|
||||||
},
|
},
|
||||||
|
[EntityReferenceFields.DATABASE]: {
|
||||||
|
label: t('label.database'),
|
||||||
|
type: 'select',
|
||||||
|
mainWidgetProps: this.mainWidgetProps,
|
||||||
|
operators: this.defaultSelectOperators,
|
||||||
|
fieldSettings: {
|
||||||
|
asyncFetch: advancedSearchClassBase.autocomplete({
|
||||||
|
searchIndex: SearchIndex.TABLE,
|
||||||
|
entityField: EntityFields.DATABASE_NAME,
|
||||||
|
isCaseInsensitive: true,
|
||||||
|
}),
|
||||||
|
useAsyncSearch: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
[EntityReferenceFields.DATABASE_SCHEMA]: {
|
||||||
|
label: t('label.database-schema'),
|
||||||
|
type: 'select',
|
||||||
|
mainWidgetProps: this.mainWidgetProps,
|
||||||
|
operators: this.defaultSelectOperators,
|
||||||
|
fieldSettings: {
|
||||||
|
asyncFetch: advancedSearchClassBase.autocomplete({
|
||||||
|
searchIndex: SearchIndex.TABLE,
|
||||||
|
entityField: EntityFields.DATABASE_SCHEMA_NAME,
|
||||||
|
isCaseInsensitive: true,
|
||||||
|
}),
|
||||||
|
useAsyncSearch: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
[EntityReferenceFields.TABLE_TYPE]: {
|
||||||
|
label: t('label.table-type'),
|
||||||
|
type: 'select',
|
||||||
|
mainWidgetProps: this.mainWidgetProps,
|
||||||
|
operators: this.defaultSelectOperators,
|
||||||
|
fieldSettings: {
|
||||||
|
asyncFetch: advancedSearchClassBase.autocomplete({
|
||||||
|
searchIndex: SearchIndex.TABLE,
|
||||||
|
entityField: EntityFields.TABLE_TYPE,
|
||||||
|
}),
|
||||||
|
useAsyncSearch: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
[EntityReferenceFields.REVIEWERS]: {
|
||||||
|
label: t('label.reviewer-plural'),
|
||||||
|
type: '!group',
|
||||||
|
mode: 'some',
|
||||||
|
defaultField: 'fullyQualifiedName',
|
||||||
|
subfields: {
|
||||||
|
fullyQualifiedName: {
|
||||||
|
label: 'Reviewers New',
|
||||||
|
type: 'select',
|
||||||
|
mainWidgetProps: this.mainWidgetProps,
|
||||||
|
operators: this.defaultSelectOperators,
|
||||||
|
fieldSettings: {
|
||||||
|
asyncFetch: advancedSearchClassBase.autocomplete({
|
||||||
|
searchIndex: [SearchIndex.USER, SearchIndex.TEAM],
|
||||||
|
entityField: EntityFields.DISPLAY_NAME_KEYWORD,
|
||||||
|
}),
|
||||||
|
useAsyncSearch: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[EntityReferenceFields.UPDATED_BY]: {
|
||||||
|
label: t('label.updated-by'),
|
||||||
|
type: 'select',
|
||||||
|
mainWidgetProps: this.mainWidgetProps,
|
||||||
|
operators: [...this.defaultSelectOperators, 'isOwner', 'isReviewer'],
|
||||||
|
fieldSettings: {
|
||||||
|
asyncFetch: advancedSearchClassBase.autocomplete({
|
||||||
|
searchIndex: [SearchIndex.USER, SearchIndex.TEAM],
|
||||||
|
entityField: EntityFields.DISPLAY_NAME_KEYWORD,
|
||||||
|
}),
|
||||||
|
useAsyncSearch: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMapFields = () => {
|
||||||
|
return this.mapFields;
|
||||||
|
};
|
||||||
|
|
||||||
|
public searchAutocomplete: (args: {
|
||||||
|
searchIndex: SearchIndex | SearchIndex[];
|
||||||
|
fieldName: string;
|
||||||
|
fieldLabel: string;
|
||||||
|
}) => SelectFieldSettings['asyncFetch'] = ({
|
||||||
|
searchIndex,
|
||||||
|
fieldName,
|
||||||
|
fieldLabel,
|
||||||
|
}) => {
|
||||||
|
return (search) => {
|
||||||
|
return searchData(
|
||||||
|
Array.isArray(search) ? search.join(',') : search ?? '',
|
||||||
|
1,
|
||||||
|
PAGE_SIZE_BASE,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
searchIndex ?? SearchIndex.DATA_ASSET,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
).then((response) => {
|
||||||
|
const data = response.data.hits.hits;
|
||||||
|
|
||||||
|
return {
|
||||||
|
values: data.map((item) => ({
|
||||||
|
value: get(item._source, fieldName, ''),
|
||||||
|
title: get(item._source, fieldLabel, ''),
|
||||||
|
})),
|
||||||
|
hasMore: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
mainWidgetProps = {
|
||||||
|
fullWidth: true,
|
||||||
|
valueLabel: t('label.criteria') + ':',
|
||||||
|
};
|
||||||
|
|
||||||
|
public autoCompleteTier: SelectFieldSettings['asyncFetch'] = async (
|
||||||
|
searchOrValues: string | (string | number)[] | null
|
||||||
|
) => {
|
||||||
|
let resolvedTierOptions: ListItem[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: tiers } = await getTags({
|
||||||
|
parent: 'Tier',
|
||||||
|
limit: 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tierFields = tiers.map((tier) => ({
|
||||||
|
title: tier.fullyQualifiedName, // tier.name,
|
||||||
|
value: tier.fullyQualifiedName,
|
||||||
|
}));
|
||||||
|
|
||||||
|
resolvedTierOptions = tierFields as ListItem[];
|
||||||
|
} catch (error) {
|
||||||
|
resolvedTierOptions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = Array.isArray(searchOrValues)
|
||||||
|
? searchOrValues.join(',')
|
||||||
|
: searchOrValues;
|
||||||
|
|
||||||
|
return {
|
||||||
|
values: !search
|
||||||
|
? resolvedTierOptions
|
||||||
|
: resolvedTierOptions.filter((tier: ListItem) =>
|
||||||
|
tier.title?.toLowerCase()?.includes(toLower(search))
|
||||||
|
),
|
||||||
|
hasMore: false,
|
||||||
|
} as AsyncFetchListValuesResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
public getCommonConfig = () => {
|
||||||
|
return {
|
||||||
|
...getFieldsByKeys(COMMON_ENTITY_FIELDS_KEYS, this.mapFields),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -389,8 +493,14 @@ class JSONLogicSearchClassBase {
|
|||||||
): Fields {
|
): Fields {
|
||||||
let configs: Fields = {};
|
let configs: Fields = {};
|
||||||
const configIndexMapping: Partial<Record<SearchIndex, Fields>> = {
|
const configIndexMapping: Partial<Record<SearchIndex, Fields>> = {
|
||||||
[SearchIndex.TABLE]: this.tableEntityFields,
|
[SearchIndex.TABLE]: getFieldsByKeys(
|
||||||
[SearchIndex.GLOSSARY_TERM]: this.glossaryEntityFields,
|
TABLE_ENTITY_FIELDS_KEYS,
|
||||||
|
this.mapFields
|
||||||
|
),
|
||||||
|
[SearchIndex.GLOSSARY_TERM]: getFieldsByKeys(
|
||||||
|
GLOSSARY_ENTITY_FIELDS_KEYS,
|
||||||
|
this.mapFields
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
entitySearchIndex.forEach((index) => {
|
entitySearchIndex.forEach((index) => {
|
||||||
@ -404,13 +514,12 @@ class JSONLogicSearchClassBase {
|
|||||||
*/
|
*/
|
||||||
public getQueryBuilderFields = ({
|
public getQueryBuilderFields = ({
|
||||||
entitySearchIndex = [SearchIndex.TABLE],
|
entitySearchIndex = [SearchIndex.TABLE],
|
||||||
tierOptions,
|
|
||||||
}: {
|
}: {
|
||||||
entitySearchIndex?: Array<SearchIndex>;
|
entitySearchIndex?: Array<SearchIndex>;
|
||||||
tierOptions?: Promise<ListValues>;
|
tierOptions?: Promise<ListValues>;
|
||||||
}) => {
|
}) => {
|
||||||
const fieldsConfig = {
|
const fieldsConfig = {
|
||||||
...this.getCommonConfig({ entitySearchIndex, tierOptions }),
|
...this.getCommonConfig(),
|
||||||
...this.getEntitySpecificQueryBuilderFields(entitySearchIndex),
|
...this.getEntitySpecificQueryBuilderFields(entitySearchIndex),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -864,3 +864,18 @@ export const getEntityTypeAggregationFilter = (
|
|||||||
|
|
||||||
return qFilter;
|
return qFilter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getFieldsByKeys = (
|
||||||
|
keys: EntityReferenceFields[],
|
||||||
|
mapFields: Record<string, FieldOrGroup>
|
||||||
|
): Record<string, FieldOrGroup> => {
|
||||||
|
const filteredFields: Record<string, FieldOrGroup> = {};
|
||||||
|
|
||||||
|
keys.forEach((key) => {
|
||||||
|
if (mapFields[key]) {
|
||||||
|
filteredFields[key] = mapFields[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return filteredFields;
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user