diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget.tsx index 59ca77737c1..42b703adff2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget.tsx @@ -21,6 +21,7 @@ import { Builder, Config, ImmutableTree, + JsonTree, Query, Utils as QbUtils, } from 'react-awesome-query-builder'; @@ -28,6 +29,7 @@ import { getExplorePath } from '../../../../../../constants/constants'; import { EntityType } from '../../../../../../enums/entity.enum'; import { SearchIndex } from '../../../../../../enums/search.enum'; import { searchQuery } from '../../../../../../rest/searchAPI'; +import { getJsonTreeFromQueryFilter } from '../../../../../../utils/QueryBuilderUtils'; import searchClassBase from '../../../../../../utils/SearchClassBase'; import { withAdvanceSearch } from '../../../../../AppRouter/withAdvanceSearch'; import { useAdvanceSearch } from '../../../../../Explore/AdvanceSearchProvider/AdvanceSearchProvider.component'; @@ -106,6 +108,21 @@ const QueryBuilderWidget: FC = ({ onChangeSearchIndex(searchIndex); }, [searchIndex]); + useEffect(() => { + if ( + !isEmpty(value) && + outputType === QueryBuilderOutputType.ELASTICSEARCH + ) { + const tree = QbUtils.checkTree( + QbUtils.loadTree( + getJsonTreeFromQueryFilter(JSON.parse(value || '')) as JsonTree + ), + config + ); + onTreeUpdate(tree, config); + } + }, []); + return (
({ + generateUUID: jest.fn(), +})); + +describe('getJsonTreeFromQueryFilter', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return a valid JSON tree structure for a given query filter', () => { + const mockUUIDs = ['uuid1', 'uuid2', 'uuid3', 'uuid4']; + ( + jest.requireMock('./StringsUtils').generateUUID as jest.Mock + ).mockImplementation(() => mockUUIDs.shift()); + const queryFilter: QueryFilterInterface = { + query: { + bool: { + must: [ + { + bool: { + must: [ + { + term: { + field1: 'value1', + }, + }, + ], + }, + }, + ], + }, + }, + }; + + const result = getJsonTreeFromQueryFilter(queryFilter); + + expect(result).toEqual({ + type: 'group', + properties: { conjunction: 'AND', not: false }, + children1: { + uuid2: { + type: 'group', + properties: { conjunction: 'AND', not: false }, + children1: { + uuid3: { + type: 'rule', + properties: { + field: 'field1', + operator: 'select_equals', + value: ['value1'], + valueSrc: ['value'], + operatorOptions: null, + valueType: ['select'], + asyncListValues: [ + { + key: 'value1', + value: 'value1', + children: 'value1', + }, + ], + }, + id: 'uuid3', + path: ['uuid1', 'uuid2', 'uuid3'], + }, + }, + id: 'uuid2', + path: ['uuid1', 'uuid2'], + }, + }, + id: 'uuid1', + path: ['uuid1'], + }); + }); + + it('should return an empty object if an error occurs', () => { + const queryFilter: QueryFilterInterface = { + query: { + bool: { + must: [], + }, + }, + }; + + const result = getJsonTreeFromQueryFilter(queryFilter); + + expect(result).toEqual({}); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/QueryBuilderUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/QueryBuilderUtils.ts new file mode 100644 index 00000000000..1f53db69ac0 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/QueryBuilderUtils.ts @@ -0,0 +1,313 @@ +/* + * Copyright 2024 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 { isUndefined } from 'lodash'; +import { + EsBoolQuery, + EsExistsQuery, + EsTerm, + EsWildCard, + QueryFieldInterface, + QueryFilterInterface, +} from '../pages/ExplorePage/ExplorePage.interface'; +import { generateUUID } from './StringsUtils'; + +export const getSelectEqualsNotEqualsProperties = ( + parentPath: Array, + field: string, + value: string, + operator: string +) => { + const id = generateUUID(); + + return { + [id]: { + type: 'rule', + properties: { + field: field, + operator, + value: [value], + valueSrc: ['value'], + operatorOptions: null, + valueType: ['select'], + asyncListValues: [ + { + key: value, + value, + children: value, + }, + ], + }, + id, + path: [...parentPath, id], + }, + }; +}; + +export const getSelectAnyInProperties = ( + parentPath: Array, + termObjects: Array +) => { + const values = termObjects.map( + (termObject) => Object.values(termObject.term)[0] + ); + const id = generateUUID(); + + return { + [id]: { + type: 'rule', + properties: { + field: Object.keys(termObjects[0].term)[0], + operator: 'select_any_in', + value: [values], + valueSrc: ['value'], + operatorOptions: null, + valueType: ['multiselect'], + asyncListValues: values.map((value) => ({ + key: value, + value, + children: value, + })), + }, + id, + path: [...parentPath, id], + }, + }; +}; + +export const getSelectNotAnyInProperties = ( + parentPath: Array, + termObjects: QueryFieldInterface[] +) => { + const values = termObjects.map( + (termObject) => + Object.values((termObject?.bool?.must_not as EsTerm)?.term)[0] + ); + const id = generateUUID(); + + return { + [id]: { + type: 'rule', + properties: { + field: Object.keys((termObjects[0].bool?.must_not as EsTerm).term)[0], + operator: 'select_not_any_in', + value: [values], + valueSrc: ['value'], + operatorOptions: null, + valueType: ['multiselect'], + asyncListValues: values.map((value) => ({ + key: value, + value, + children: value, + })), + }, + id, + path: [...parentPath, id], + }, + }; +}; + +export const getCommonFieldProperties = ( + parentPath: Array, + field: string, + operator: string, + value?: string +) => { + const id = generateUUID(); + + return { + [id]: { + type: 'rule', + properties: { + field, + operator, + value: isUndefined(value) ? [] : [value.replaceAll(/(^\*)|(\*$)/g, '')], + valueSrc: isUndefined(value) ? [] : ['value'], + operatorOptions: null, + valueType: isUndefined(value) ? [] : ['text'], + }, + id, + path: [...parentPath, id], + }, + }; +}; + +export const getEqualFieldProperties = ( + parentPath: Array, + value: boolean +) => { + const id = generateUUID(); + + return { + [id]: { + type: 'rule', + properties: { + field: 'deleted', + operator: 'equal', + value: [value], + valueSrc: ['value'], + operatorOptions: null, + valueType: ['boolean'], + }, + id, + path: [...parentPath, id], + }, + }; +}; + +export const getJsonTreePropertyFromQueryFilter = ( + parentPath: Array, + queryFilter: QueryFieldInterface[] +) => { + const convertedObj = queryFilter.reduce( + (acc, curr: QueryFieldInterface): Record => { + if (!isUndefined(curr.term?.deleted)) { + return { + ...acc, + ...getEqualFieldProperties(parentPath, curr.term?.deleted as boolean), + }; + } else if (!isUndefined(curr.term)) { + return { + ...acc, + ...getSelectEqualsNotEqualsProperties( + parentPath, + Object.keys(curr.term)[0], + Object.values(curr.term)[0] as string, + 'select_equals' + ), + }; + } else if ( + !isUndefined((curr.bool?.must_not as QueryFieldInterface).term) + ) { + return { + ...acc, + ...getSelectEqualsNotEqualsProperties( + parentPath, + Object.keys((curr.bool?.must_not as EsTerm)?.term)[0], + Object.values((curr.bool?.must_not as EsTerm)?.term)[0] as string, + 'select_not_equals' + ), + }; + } else if ( + !isUndefined( + ((curr.bool?.should as QueryFieldInterface[])?.[0] as EsTerm)?.term + ) + ) { + return { + ...acc, + ...getSelectAnyInProperties( + parentPath, + curr?.bool?.should as EsTerm[] + ), + }; + } else if ( + !isUndefined( + ( + (curr.bool?.should as QueryFieldInterface[])?.[0]?.bool + ?.must_not as EsTerm + )?.term + ) + ) { + return { + ...acc, + ...getSelectNotAnyInProperties( + parentPath, + curr?.bool?.should as QueryFieldInterface[] + ), + }; + } else if ( + !isUndefined( + (curr.bool?.must_not as QueryFieldInterface)?.exists?.field + ) + ) { + return { + ...acc, + ...getCommonFieldProperties( + parentPath, + (curr.bool?.must_not as QueryFieldInterface)?.exists + ?.field as string, + 'is_null' + ), + }; + } else if (!isUndefined(curr.exists?.field)) { + return { + ...acc, + ...getCommonFieldProperties( + parentPath, + (curr.exists as EsExistsQuery).field, + 'is_not_null' + ), + }; + } else if (!isUndefined((curr as EsWildCard).wildcard)) { + return { + ...acc, + ...getCommonFieldProperties( + parentPath, + Object.keys((curr as EsWildCard).wildcard)[0], + 'like', + Object.values((curr as EsWildCard).wildcard)[0]?.value + ), + }; + } else if (!isUndefined((curr.bool?.must_not as EsWildCard)?.wildcard)) { + return { + ...acc, + ...getCommonFieldProperties( + parentPath, + Object.keys( + (curr.bool?.must_not as EsWildCard)?.wildcard + )[0] as string, + 'not_like', + Object.values((curr.bool?.must_not as EsWildCard)?.wildcard)[0] + ?.value + ), + }; + } + + return acc; + }, + {} as Record + ); + + return convertedObj; +}; + +export const getJsonTreeFromQueryFilter = ( + queryFilter: QueryFilterInterface +) => { + try { + const id1 = generateUUID(); + const id2 = generateUUID(); + const mustFilters = queryFilter?.query?.bool?.must as QueryFieldInterface[]; + + return { + type: 'group', + properties: { conjunction: 'AND', not: false }, + children1: { + [id2]: { + type: 'group', + properties: { conjunction: 'AND', not: false }, + children1: getJsonTreePropertyFromQueryFilter( + [id1, id2], + (mustFilters?.[0]?.bool as EsBoolQuery) + .must as QueryFieldInterface[] + ), + id: id2, + path: [id1, id2], + }, + }, + id: id1, + path: [id1], + }; + } catch { + return {}; + } +};