mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-18 14:06:59 +00:00
fix the search for custom properties in advance search for some types (#19113)
* fix the search in advance search custom properties * modify v1/metadata/types/customProperties api to get customPropertyConfig along with name and type * added config list in the entity ref list * fix type of duration, timestamp and other types * minor ui changes * remove customProperty type which are not supported for now * added playwright test around the duration type property * fix flaky test --------- Co-authored-by: sonikashah <sonikashah94@gmail.com> (cherry picked from commit 2179b43232803a9ebb7465fb1d545b887d68252c)
This commit is contained in:
parent
9a63d776b5
commit
cb733a2ad8
@ -32,7 +32,7 @@ import org.openmetadata.service.jdbi3.TypeRepository;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class SchemaFieldExtractor {
|
public class SchemaFieldExtractor {
|
||||||
|
|
||||||
private static final Map<String, Map<String, String>> entityFieldsCache =
|
private static final Map<String, Map<String, FieldDefinition>> entityFieldsCache =
|
||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public SchemaFieldExtractor() {
|
public SchemaFieldExtractor() {
|
||||||
@ -55,7 +55,7 @@ public class SchemaFieldExtractor {
|
|||||||
Schema mainSchema = loadMainSchema(schemaPath, entityType, schemaUri, schemaClient);
|
Schema mainSchema = loadMainSchema(schemaPath, entityType, schemaUri, schemaClient);
|
||||||
|
|
||||||
// Extract fields from the schema
|
// Extract fields from the schema
|
||||||
Map<String, String> fieldTypesMap = new LinkedHashMap<>();
|
Map<String, FieldDefinition> fieldTypesMap = new LinkedHashMap<>();
|
||||||
Deque<Schema> processingStack = new ArrayDeque<>();
|
Deque<Schema> processingStack = new ArrayDeque<>();
|
||||||
Set<String> processedFields = new HashSet<>();
|
Set<String> processedFields = new HashSet<>();
|
||||||
extractFieldsFromSchema(mainSchema, "", fieldTypesMap, processingStack, processedFields);
|
extractFieldsFromSchema(mainSchema, "", fieldTypesMap, processingStack, processedFields);
|
||||||
@ -75,7 +75,7 @@ public class SchemaFieldExtractor {
|
|||||||
SchemaClient schemaClient = new CustomSchemaClient(schemaUri);
|
SchemaClient schemaClient = new CustomSchemaClient(schemaUri);
|
||||||
Deque<Schema> processingStack = new ArrayDeque<>();
|
Deque<Schema> processingStack = new ArrayDeque<>();
|
||||||
Set<String> processedFields = new HashSet<>();
|
Set<String> processedFields = new HashSet<>();
|
||||||
Map<String, String> fieldTypesMap = entityFieldsCache.get(entityType);
|
Map<String, FieldDefinition> fieldTypesMap = entityFieldsCache.get(entityType);
|
||||||
addCustomProperties(
|
addCustomProperties(
|
||||||
typeEntity, schemaUri, schemaClient, fieldTypesMap, processingStack, processedFields);
|
typeEntity, schemaUri, schemaClient, fieldTypesMap, processingStack, processedFields);
|
||||||
return convertMapToFieldList(fieldTypesMap);
|
return convertMapToFieldList(fieldTypesMap);
|
||||||
@ -90,7 +90,7 @@ public class SchemaFieldExtractor {
|
|||||||
SchemaClient schemaClient = new CustomSchemaClient(schemaUri);
|
SchemaClient schemaClient = new CustomSchemaClient(schemaUri);
|
||||||
EntityUtil.Fields fieldsParam = new EntityUtil.Fields(Set.of("customProperties"));
|
EntityUtil.Fields fieldsParam = new EntityUtil.Fields(Set.of("customProperties"));
|
||||||
Type typeEntity = repository.getByName(uriInfo, entityType, fieldsParam, Include.ALL, false);
|
Type typeEntity = repository.getByName(uriInfo, entityType, fieldsParam, Include.ALL, false);
|
||||||
Map<String, String> fieldTypesMap = new LinkedHashMap<>();
|
Map<String, FieldDefinition> fieldTypesMap = new LinkedHashMap<>();
|
||||||
Set<String> processedFields = new HashSet<>();
|
Set<String> processedFields = new HashSet<>();
|
||||||
Deque<Schema> processingStack = new ArrayDeque<>();
|
Deque<Schema> processingStack = new ArrayDeque<>();
|
||||||
addCustomProperties(
|
addCustomProperties(
|
||||||
@ -170,7 +170,7 @@ public class SchemaFieldExtractor {
|
|||||||
private static void extractFieldsFromSchema(
|
private static void extractFieldsFromSchema(
|
||||||
Schema schema,
|
Schema schema,
|
||||||
String parentPath,
|
String parentPath,
|
||||||
Map<String, String> fieldTypesMap,
|
Map<String, FieldDefinition> fieldTypesMap,
|
||||||
Deque<Schema> processingStack,
|
Deque<Schema> processingStack,
|
||||||
Set<String> processedFields) {
|
Set<String> processedFields) {
|
||||||
if (processingStack.contains(schema)) {
|
if (processingStack.contains(schema)) {
|
||||||
@ -206,7 +206,8 @@ public class SchemaFieldExtractor {
|
|||||||
arraySchema, fullFieldName, fieldTypesMap, processingStack, processedFields);
|
arraySchema, fullFieldName, fieldTypesMap, processingStack, processedFields);
|
||||||
} else {
|
} else {
|
||||||
String fieldType = mapSchemaTypeToSimpleType(fieldSchema);
|
String fieldType = mapSchemaTypeToSimpleType(fieldSchema);
|
||||||
fieldTypesMap.putIfAbsent(fullFieldName, fieldType);
|
fieldTypesMap.putIfAbsent(
|
||||||
|
fullFieldName, new FieldDefinition(fullFieldName, fieldType, null));
|
||||||
processedFields.add(fullFieldName);
|
processedFields.add(fullFieldName);
|
||||||
LOG.debug("Added field '{}', Type: '{}'", fullFieldName, fieldType);
|
LOG.debug("Added field '{}', Type: '{}'", fullFieldName, fieldType);
|
||||||
// Recursively process nested objects or arrays
|
// Recursively process nested objects or arrays
|
||||||
@ -220,7 +221,7 @@ public class SchemaFieldExtractor {
|
|||||||
handleArraySchema(arraySchema, parentPath, fieldTypesMap, processingStack, processedFields);
|
handleArraySchema(arraySchema, parentPath, fieldTypesMap, processingStack, processedFields);
|
||||||
} else {
|
} else {
|
||||||
String fieldType = mapSchemaTypeToSimpleType(schema);
|
String fieldType = mapSchemaTypeToSimpleType(schema);
|
||||||
fieldTypesMap.putIfAbsent(parentPath, fieldType);
|
fieldTypesMap.putIfAbsent(parentPath, new FieldDefinition(parentPath, fieldType, null));
|
||||||
LOG.debug("Added field '{}', Type: '{}'", parentPath, fieldType);
|
LOG.debug("Added field '{}', Type: '{}'", parentPath, fieldType);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@ -231,7 +232,7 @@ public class SchemaFieldExtractor {
|
|||||||
private static void handleReferenceSchema(
|
private static void handleReferenceSchema(
|
||||||
ReferenceSchema referenceSchema,
|
ReferenceSchema referenceSchema,
|
||||||
String fullFieldName,
|
String fullFieldName,
|
||||||
Map<String, String> fieldTypesMap,
|
Map<String, FieldDefinition> fieldTypesMap,
|
||||||
Deque<Schema> processingStack,
|
Deque<Schema> processingStack,
|
||||||
Set<String> processedFields) {
|
Set<String> processedFields) {
|
||||||
|
|
||||||
@ -239,7 +240,8 @@ public class SchemaFieldExtractor {
|
|||||||
String referenceType = determineReferenceType(refUri);
|
String referenceType = determineReferenceType(refUri);
|
||||||
|
|
||||||
if (referenceType != null) {
|
if (referenceType != null) {
|
||||||
fieldTypesMap.putIfAbsent(fullFieldName, referenceType);
|
fieldTypesMap.putIfAbsent(
|
||||||
|
fullFieldName, new FieldDefinition(fullFieldName, referenceType, null));
|
||||||
processedFields.add(fullFieldName);
|
processedFields.add(fullFieldName);
|
||||||
LOG.debug("Added field '{}', Type: '{}'", fullFieldName, referenceType);
|
LOG.debug("Added field '{}', Type: '{}'", fullFieldName, referenceType);
|
||||||
if (referenceType.startsWith("array<") && referenceType.endsWith(">")) {
|
if (referenceType.startsWith("array<") && referenceType.endsWith(">")) {
|
||||||
@ -255,7 +257,7 @@ public class SchemaFieldExtractor {
|
|||||||
referredSchema, fullFieldName, fieldTypesMap, processingStack, processedFields);
|
referredSchema, fullFieldName, fieldTypesMap, processingStack, processedFields);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fieldTypesMap.putIfAbsent(fullFieldName, "object");
|
fieldTypesMap.putIfAbsent(fullFieldName, new FieldDefinition(fullFieldName, "object", null));
|
||||||
processedFields.add(fullFieldName);
|
processedFields.add(fullFieldName);
|
||||||
LOG.debug("Added field '{}', Type: 'object'", fullFieldName);
|
LOG.debug("Added field '{}', Type: 'object'", fullFieldName);
|
||||||
extractFieldsFromSchema(
|
extractFieldsFromSchema(
|
||||||
@ -270,7 +272,7 @@ public class SchemaFieldExtractor {
|
|||||||
private static void handleArraySchema(
|
private static void handleArraySchema(
|
||||||
ArraySchema arraySchema,
|
ArraySchema arraySchema,
|
||||||
String fullFieldName,
|
String fullFieldName,
|
||||||
Map<String, String> fieldTypesMap,
|
Map<String, FieldDefinition> fieldTypesMap,
|
||||||
Deque<Schema> processingStack,
|
Deque<Schema> processingStack,
|
||||||
Set<String> processedFields) {
|
Set<String> processedFields) {
|
||||||
|
|
||||||
@ -282,7 +284,8 @@ public class SchemaFieldExtractor {
|
|||||||
|
|
||||||
if (itemsReferenceType != null) {
|
if (itemsReferenceType != null) {
|
||||||
String arrayFieldType = "array<" + itemsReferenceType + ">";
|
String arrayFieldType = "array<" + itemsReferenceType + ">";
|
||||||
fieldTypesMap.putIfAbsent(fullFieldName, arrayFieldType);
|
fieldTypesMap.putIfAbsent(
|
||||||
|
fullFieldName, new FieldDefinition(fullFieldName, arrayFieldType, null));
|
||||||
processedFields.add(fullFieldName);
|
processedFields.add(fullFieldName);
|
||||||
LOG.debug("Added field '{}', Type: '{}'", fullFieldName, arrayFieldType);
|
LOG.debug("Added field '{}', Type: '{}'", fullFieldName, arrayFieldType);
|
||||||
Schema referredItemsSchema = itemsReferenceSchema.getReferredSchema();
|
Schema referredItemsSchema = itemsReferenceSchema.getReferredSchema();
|
||||||
@ -292,7 +295,8 @@ public class SchemaFieldExtractor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
String arrayType = mapSchemaTypeToSimpleType(itemsSchema);
|
String arrayType = mapSchemaTypeToSimpleType(itemsSchema);
|
||||||
fieldTypesMap.putIfAbsent(fullFieldName, "array<" + arrayType + ">");
|
fieldTypesMap.putIfAbsent(
|
||||||
|
fullFieldName, new FieldDefinition(fullFieldName, "array<" + arrayType + ">", null));
|
||||||
processedFields.add(fullFieldName);
|
processedFields.add(fullFieldName);
|
||||||
LOG.debug("Added field '{}', Type: 'array<{}>'", fullFieldName, arrayType);
|
LOG.debug("Added field '{}', Type: 'array<{}>'", fullFieldName, arrayType);
|
||||||
|
|
||||||
@ -306,7 +310,7 @@ public class SchemaFieldExtractor {
|
|||||||
Type typeEntity,
|
Type typeEntity,
|
||||||
String schemaUri,
|
String schemaUri,
|
||||||
SchemaClient schemaClient,
|
SchemaClient schemaClient,
|
||||||
Map<String, String> fieldTypesMap,
|
Map<String, FieldDefinition> fieldTypesMap,
|
||||||
Deque<Schema> processingStack,
|
Deque<Schema> processingStack,
|
||||||
Set<String> processedFields) {
|
Set<String> processedFields) {
|
||||||
if (typeEntity == null || typeEntity.getCustomProperties() == null) {
|
if (typeEntity == null || typeEntity.getCustomProperties() == null) {
|
||||||
@ -320,9 +324,13 @@ public class SchemaFieldExtractor {
|
|||||||
|
|
||||||
LOG.debug("Processing custom property '{}'", fullFieldName);
|
LOG.debug("Processing custom property '{}'", fullFieldName);
|
||||||
|
|
||||||
|
Object customPropertyConfigObj = customProperty.getCustomPropertyConfig();
|
||||||
|
|
||||||
if (isEntityReferenceList(propertyType)) {
|
if (isEntityReferenceList(propertyType)) {
|
||||||
String referenceType = "array<entityReference>";
|
String referenceType = "array<entityReference>";
|
||||||
fieldTypesMap.putIfAbsent(fullFieldName, referenceType);
|
FieldDefinition referenceFieldDefinition =
|
||||||
|
new FieldDefinition(fullFieldName, referenceType, customPropertyConfigObj);
|
||||||
|
fieldTypesMap.putIfAbsent(fullFieldName, referenceFieldDefinition);
|
||||||
processedFields.add(fullFieldName);
|
processedFields.add(fullFieldName);
|
||||||
LOG.debug("Added custom property '{}', Type: '{}'", fullFieldName, referenceType);
|
LOG.debug("Added custom property '{}', Type: '{}'", fullFieldName, referenceType);
|
||||||
|
|
||||||
@ -337,7 +345,9 @@ public class SchemaFieldExtractor {
|
|||||||
}
|
}
|
||||||
} else if (isEntityReference(propertyType)) {
|
} else if (isEntityReference(propertyType)) {
|
||||||
String referenceType = "entityReference";
|
String referenceType = "entityReference";
|
||||||
fieldTypesMap.putIfAbsent(fullFieldName, referenceType);
|
FieldDefinition referenceFieldDefinition =
|
||||||
|
new FieldDefinition(fullFieldName, referenceType, customPropertyConfigObj);
|
||||||
|
fieldTypesMap.putIfAbsent(fullFieldName, referenceFieldDefinition);
|
||||||
processedFields.add(fullFieldName);
|
processedFields.add(fullFieldName);
|
||||||
LOG.debug("Added custom property '{}', Type: '{}'", fullFieldName, referenceType);
|
LOG.debug("Added custom property '{}', Type: '{}'", fullFieldName, referenceType);
|
||||||
|
|
||||||
@ -351,17 +361,22 @@ public class SchemaFieldExtractor {
|
|||||||
fullFieldName);
|
fullFieldName);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fieldTypesMap.putIfAbsent(fullFieldName, propertyType);
|
FieldDefinition entityFieldDefinition =
|
||||||
|
new FieldDefinition(fullFieldName, propertyType, customPropertyConfigObj);
|
||||||
|
fieldTypesMap.putIfAbsent(fullFieldName, entityFieldDefinition);
|
||||||
processedFields.add(fullFieldName);
|
processedFields.add(fullFieldName);
|
||||||
LOG.debug("Added custom property '{}', Type: '{}'", fullFieldName, propertyType);
|
LOG.debug("Added custom property '{}', Type: '{}'", fullFieldName, propertyType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<FieldDefinition> convertMapToFieldList(Map<String, String> fieldTypesMap) {
|
private List<FieldDefinition> convertMapToFieldList(Map<String, FieldDefinition> fieldTypesMap) {
|
||||||
List<FieldDefinition> fieldsList = new ArrayList<>();
|
List<FieldDefinition> fieldsList = new ArrayList<>();
|
||||||
for (Map.Entry<String, String> entry : fieldTypesMap.entrySet()) {
|
for (Map.Entry<String, FieldDefinition> entry : fieldTypesMap.entrySet()) {
|
||||||
fieldsList.add(new FieldDefinition(entry.getKey(), entry.getValue()));
|
FieldDefinition fieldDef = entry.getValue();
|
||||||
|
fieldsList.add(
|
||||||
|
new FieldDefinition(
|
||||||
|
fieldDef.getName(), fieldDef.getType(), fieldDef.getCustomPropertyConfig()));
|
||||||
}
|
}
|
||||||
return fieldsList;
|
return fieldsList;
|
||||||
}
|
}
|
||||||
@ -622,10 +637,12 @@ public class SchemaFieldExtractor {
|
|||||||
public static class FieldDefinition {
|
public static class FieldDefinition {
|
||||||
private String name;
|
private String name;
|
||||||
private String type;
|
private String type;
|
||||||
|
private Object customPropertyConfig;
|
||||||
|
|
||||||
public FieldDefinition(String name, String type) {
|
public FieldDefinition(String name, String type, Object customPropertyConfig) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
this.customPropertyConfig = customPropertyConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* 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 test, { expect } from '@playwright/test';
|
||||||
|
import { CUSTOM_PROPERTIES_ENTITIES } from '../../constant/customProperty';
|
||||||
|
import { GlobalSettingOptions } from '../../constant/settings';
|
||||||
|
import { SidebarItem } from '../../constant/sidebar';
|
||||||
|
import { TableClass } from '../../support/entity/TableClass';
|
||||||
|
import {
|
||||||
|
selectOption,
|
||||||
|
showAdvancedSearchDialog,
|
||||||
|
} from '../../utils/advancedSearch';
|
||||||
|
import { advanceSearchSaveFilter } from '../../utils/advancedSearchCustomProperty';
|
||||||
|
import { createNewPage, redirectToHomePage, uuid } from '../../utils/common';
|
||||||
|
import { addCustomPropertiesForEntity } from '../../utils/customProperty';
|
||||||
|
import { settingClick, sidebarClick } from '../../utils/sidebar';
|
||||||
|
|
||||||
|
// use the admin user to login
|
||||||
|
test.use({ storageState: 'playwright/.auth/admin.json' });
|
||||||
|
|
||||||
|
test.describe('Advanced Search Custom Property', () => {
|
||||||
|
const table = new TableClass();
|
||||||
|
const durationPropertyName = `pwCustomPropertyDurationTest${uuid()}`;
|
||||||
|
const durationPropertyValue = 'PT1H30M';
|
||||||
|
|
||||||
|
test.beforeAll('Setup pre-requests', async ({ browser }) => {
|
||||||
|
const { apiContext, afterAction } = await createNewPage(browser);
|
||||||
|
await table.create(apiContext);
|
||||||
|
await afterAction();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll('Cleanup', async ({ browser }) => {
|
||||||
|
const { apiContext, afterAction } = await createNewPage(browser);
|
||||||
|
await table.delete(apiContext);
|
||||||
|
await afterAction();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Create, Assign and Test Advance Search for Duration', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
test.slow(true);
|
||||||
|
|
||||||
|
await redirectToHomePage(page);
|
||||||
|
|
||||||
|
await test.step('Create and Assign Custom Property Value', async () => {
|
||||||
|
await settingClick(page, GlobalSettingOptions.TABLES, true);
|
||||||
|
|
||||||
|
await addCustomPropertiesForEntity({
|
||||||
|
page,
|
||||||
|
propertyName: durationPropertyName,
|
||||||
|
customPropertyData: CUSTOM_PROPERTIES_ENTITIES['entity_table'],
|
||||||
|
customType: 'Duration',
|
||||||
|
});
|
||||||
|
|
||||||
|
await table.visitEntityPage(page);
|
||||||
|
|
||||||
|
await page.getByTestId('custom_properties').click(); // Tab Click
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId(`custom-property-${durationPropertyName}-card`)
|
||||||
|
.locator('svg')
|
||||||
|
.click(); // Add Custom Property Value
|
||||||
|
|
||||||
|
await page.getByTestId('duration-input').fill(durationPropertyValue);
|
||||||
|
|
||||||
|
const saveResponse = page.waitForResponse('/api/v1/tables/*');
|
||||||
|
await page.getByTestId('inline-save-btn').click();
|
||||||
|
await saveResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Verify Duration Type in Advance Search ', async () => {
|
||||||
|
await sidebarClick(page, SidebarItem.EXPLORE);
|
||||||
|
|
||||||
|
await showAdvancedSearchDialog(page);
|
||||||
|
|
||||||
|
const ruleLocator = page.locator('.rule').nth(0);
|
||||||
|
|
||||||
|
// Perform click on rule field
|
||||||
|
await selectOption(
|
||||||
|
page,
|
||||||
|
ruleLocator.locator('.rule--field .ant-select'),
|
||||||
|
'Custom Properties'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Perform click on custom property type to filter
|
||||||
|
await selectOption(
|
||||||
|
page,
|
||||||
|
ruleLocator.locator('.rule--field .ant-select'),
|
||||||
|
durationPropertyName
|
||||||
|
);
|
||||||
|
|
||||||
|
const inputElement = ruleLocator.locator(
|
||||||
|
'.rule--widget--TEXT input[type="text"]'
|
||||||
|
);
|
||||||
|
|
||||||
|
await inputElement.fill(durationPropertyValue);
|
||||||
|
|
||||||
|
await advanceSearchSaveFilter(page, durationPropertyValue);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByTestId(
|
||||||
|
`table-data-card_${table.entityResponseData.fullyQualifiedName}`
|
||||||
|
)
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Check around the Partial Search Value
|
||||||
|
const partialSearchValue = durationPropertyValue.slice(0, 3);
|
||||||
|
|
||||||
|
await page.getByTestId('advance-search-filter-btn').click();
|
||||||
|
|
||||||
|
await expect(page.locator('[role="dialog"].ant-modal')).toBeVisible();
|
||||||
|
|
||||||
|
// Perform click on operator
|
||||||
|
await selectOption(
|
||||||
|
page,
|
||||||
|
ruleLocator.locator('.rule--operator .ant-select'),
|
||||||
|
'Contains'
|
||||||
|
);
|
||||||
|
|
||||||
|
await inputElement.fill(partialSearchValue);
|
||||||
|
|
||||||
|
await advanceSearchSaveFilter(page, partialSearchValue);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByTestId(
|
||||||
|
`table-data-card_${table.entityResponseData.fullyQualifiedName}`
|
||||||
|
)
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* 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 { expect, Page } from '@playwright/test';
|
||||||
|
|
||||||
|
export const advanceSearchSaveFilter = async (
|
||||||
|
page: Page,
|
||||||
|
propertyValue: string
|
||||||
|
) => {
|
||||||
|
const searchResponse = page.waitForResponse(
|
||||||
|
'/api/v1/search/query?*index=dataAsset&from=0&size=10*'
|
||||||
|
);
|
||||||
|
await page.getByTestId('apply-btn').click();
|
||||||
|
|
||||||
|
const res = await searchResponse;
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
expect(JSON.stringify(json)).toContain(propertyValue);
|
||||||
|
};
|
@ -27,16 +27,15 @@ import {
|
|||||||
JsonTree,
|
JsonTree,
|
||||||
Utils as QbUtils,
|
Utils as QbUtils,
|
||||||
ValueField,
|
ValueField,
|
||||||
|
ValueSource,
|
||||||
} from 'react-awesome-query-builder';
|
} from 'react-awesome-query-builder';
|
||||||
import { useHistory, useParams } from 'react-router-dom';
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
import {
|
import { emptyJsonTree } from '../../../constants/AdvancedSearch.constants';
|
||||||
emptyJsonTree,
|
|
||||||
TEXT_FIELD_OPERATORS,
|
|
||||||
} from '../../../constants/AdvancedSearch.constants';
|
|
||||||
import { SearchIndex } from '../../../enums/search.enum';
|
import { SearchIndex } from '../../../enums/search.enum';
|
||||||
import useCustomLocation from '../../../hooks/useCustomLocation/useCustomLocation';
|
import useCustomLocation from '../../../hooks/useCustomLocation/useCustomLocation';
|
||||||
import { TabsInfoData } from '../../../pages/ExplorePage/ExplorePage.interface';
|
import { TabsInfoData } from '../../../pages/ExplorePage/ExplorePage.interface';
|
||||||
import { getAllCustomProperties } from '../../../rest/metadataTypeAPI';
|
import { getAllCustomProperties } from '../../../rest/metadataTypeAPI';
|
||||||
|
import advancedSearchClassBase from '../../../utils/AdvancedSearchClassBase';
|
||||||
import {
|
import {
|
||||||
getTierOptions,
|
getTierOptions,
|
||||||
getTreeConfig,
|
getTreeConfig,
|
||||||
@ -225,15 +224,24 @@ export const AdvanceSearchProvider = ({
|
|||||||
|
|
||||||
Object.entries(res).forEach(([_, fields]) => {
|
Object.entries(res).forEach(([_, fields]) => {
|
||||||
if (Array.isArray(fields) && fields.length > 0) {
|
if (Array.isArray(fields) && fields.length > 0) {
|
||||||
fields.forEach((field: { name: string; type: string }) => {
|
fields.forEach(
|
||||||
if (field.name && field.type) {
|
(field: {
|
||||||
subfields[field.name] = {
|
name: string;
|
||||||
type: 'text',
|
type: string;
|
||||||
valueSources: ['value'],
|
customPropertyConfig: {
|
||||||
operators: TEXT_FIELD_OPERATORS,
|
config: string | string[];
|
||||||
};
|
};
|
||||||
|
}) => {
|
||||||
|
if (field.name && field.type) {
|
||||||
|
const { subfieldsKey, dataObject } =
|
||||||
|
advancedSearchClassBase.getCustomPropertiesSubFields(field);
|
||||||
|
subfields[subfieldsKey] = {
|
||||||
|
...dataObject,
|
||||||
|
valueSources: dataObject.valueSources as ValueSource[],
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -51,3 +51,7 @@ export interface AdvanceSearchContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type FilterObject = Record<string, string[]>;
|
export type FilterObject = Record<string, string[]>;
|
||||||
|
export interface CustomPropertyEnumConfig {
|
||||||
|
multiSelect: boolean;
|
||||||
|
values: string[];
|
||||||
|
}
|
||||||
|
@ -905,7 +905,7 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="d-flex justify-center flex-wrap gap-3"
|
className="d-flex justify-center flex-wrap gap-2 py-2"
|
||||||
data-testid="time-interval-value">
|
data-testid="time-interval-value">
|
||||||
<div className="d-flex flex-column gap-2 items-center">
|
<div className="d-flex flex-column gap-2 items-center">
|
||||||
<StartTimeIcon height={30} width={30} />
|
<StartTimeIcon height={30} width={30} />
|
||||||
|
@ -299,6 +299,16 @@ export const TEXT_FIELD_OPERATORS = [
|
|||||||
'is_null',
|
'is_null',
|
||||||
'is_not_null',
|
'is_not_null',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const RANGE_FIELD_OPERATORS = ['between', 'not_between'];
|
||||||
|
|
||||||
|
export const LIST_VALUE_OPERATORS = [
|
||||||
|
'select_equals',
|
||||||
|
'select_not_equals',
|
||||||
|
'is_null',
|
||||||
|
'is_not_null',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a query builder tree with a group containing an empty rule
|
* Generates a query builder tree with a group containing an empty rule
|
||||||
*/
|
*/
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2024 Collate.
|
* Copyright 2025 Collate.
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
@ -10,9 +10,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
|
||||||
/**
|
|
||||||
* Airbyte Metadata Database Connection Config
|
* Airbyte Metadata Database Connection Config
|
||||||
*/
|
*/
|
||||||
export interface AirbyteConnection {
|
export interface AirbyteConnection {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2024 Collate.
|
* Copyright 2025 Collate.
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
|
@ -255,6 +255,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rule--widget--NUMBER {
|
||||||
|
.widget--valuesrc {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.ant-input-number {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.rule--widget {
|
.rule--widget {
|
||||||
min-width: 180px;
|
min-width: 180px;
|
||||||
.ant-select,
|
.ant-select,
|
||||||
|
@ -22,11 +22,20 @@ import {
|
|||||||
SelectFieldSettings,
|
SelectFieldSettings,
|
||||||
} from 'react-awesome-query-builder';
|
} from 'react-awesome-query-builder';
|
||||||
import AntdConfig from 'react-awesome-query-builder/lib/config/antd';
|
import AntdConfig from 'react-awesome-query-builder/lib/config/antd';
|
||||||
import { SEARCH_INDICES_WITH_COLUMNS_FIELD } from '../constants/AdvancedSearch.constants';
|
import { CustomPropertyEnumConfig } from '../components/Explore/AdvanceSearchProvider/AdvanceSearchProvider.interface';
|
||||||
|
import {
|
||||||
|
LIST_VALUE_OPERATORS,
|
||||||
|
RANGE_FIELD_OPERATORS,
|
||||||
|
SEARCH_INDICES_WITH_COLUMNS_FIELD,
|
||||||
|
TEXT_FIELD_OPERATORS,
|
||||||
|
} from '../constants/AdvancedSearch.constants';
|
||||||
import { EntityFields, SuggestionField } from '../enums/AdvancedSearch.enum';
|
import { EntityFields, SuggestionField } from '../enums/AdvancedSearch.enum';
|
||||||
import { SearchIndex } from '../enums/search.enum';
|
import { SearchIndex } from '../enums/search.enum';
|
||||||
import { getAggregateFieldOptions } from '../rest/miscAPI';
|
import { getAggregateFieldOptions } from '../rest/miscAPI';
|
||||||
import { renderAdvanceSearchButtons } from './AdvancedSearchUtils';
|
import {
|
||||||
|
getCustomPropertyAdvanceSearchEnumOptions,
|
||||||
|
renderAdvanceSearchButtons,
|
||||||
|
} from './AdvancedSearchUtils';
|
||||||
import { getCombinedQueryFilterObject } from './ExplorePage/ExplorePageUtils';
|
import { getCombinedQueryFilterObject } from './ExplorePage/ExplorePageUtils';
|
||||||
import { renderQueryBuilderFilterButtons } from './QueryBuilderUtils';
|
import { renderQueryBuilderFilterButtons } from './QueryBuilderUtils';
|
||||||
|
|
||||||
@ -208,12 +217,7 @@ class AdvancedSearchClassBase {
|
|||||||
[EntityFields.COLUMN_DESCRIPTION_STATUS]: {
|
[EntityFields.COLUMN_DESCRIPTION_STATUS]: {
|
||||||
label: t('label.column-description'),
|
label: t('label.column-description'),
|
||||||
type: 'select',
|
type: 'select',
|
||||||
operators: [
|
operators: LIST_VALUE_OPERATORS,
|
||||||
'select_equals',
|
|
||||||
'select_not_equals',
|
|
||||||
'is_null',
|
|
||||||
'is_not_null',
|
|
||||||
],
|
|
||||||
mainWidgetProps: this.mainWidgetProps,
|
mainWidgetProps: this.mainWidgetProps,
|
||||||
valueSources: ['value'],
|
valueSources: ['value'],
|
||||||
fieldSettings: {
|
fieldSettings: {
|
||||||
@ -680,12 +684,7 @@ class AdvancedSearchClassBase {
|
|||||||
descriptionStatus: {
|
descriptionStatus: {
|
||||||
label: t('label.description'),
|
label: t('label.description'),
|
||||||
type: 'select',
|
type: 'select',
|
||||||
operators: [
|
operators: LIST_VALUE_OPERATORS,
|
||||||
'select_equals',
|
|
||||||
'select_not_equals',
|
|
||||||
'is_null',
|
|
||||||
'is_not_null',
|
|
||||||
],
|
|
||||||
mainWidgetProps: this.mainWidgetProps,
|
mainWidgetProps: this.mainWidgetProps,
|
||||||
valueSources: ['value'],
|
valueSources: ['value'],
|
||||||
fieldSettings: {
|
fieldSettings: {
|
||||||
@ -899,6 +898,86 @@ class AdvancedSearchClassBase {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public getCustomPropertiesSubFields(field: {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
customPropertyConfig: {
|
||||||
|
config: string | string[] | CustomPropertyEnumConfig;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
{
|
||||||
|
switch (field.type) {
|
||||||
|
case 'array<entityReference>':
|
||||||
|
case 'entityReference':
|
||||||
|
return {
|
||||||
|
subfieldsKey: field.name + `.displayName`,
|
||||||
|
dataObject: {
|
||||||
|
type: 'select',
|
||||||
|
label: field.name,
|
||||||
|
fieldSettings: {
|
||||||
|
asyncFetch: this.autocomplete({
|
||||||
|
searchIndex: (
|
||||||
|
(field.customPropertyConfig.config ?? []) as string[]
|
||||||
|
).join(',') as SearchIndex,
|
||||||
|
entityField: EntityFields.DISPLAY_NAME_KEYWORD,
|
||||||
|
}),
|
||||||
|
useAsyncSearch: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'enum':
|
||||||
|
return {
|
||||||
|
subfieldsKey: field.name,
|
||||||
|
dataObject: {
|
||||||
|
type: 'select',
|
||||||
|
operators: LIST_VALUE_OPERATORS,
|
||||||
|
fieldSettings: {
|
||||||
|
listValues: getCustomPropertyAdvanceSearchEnumOptions(
|
||||||
|
(
|
||||||
|
field.customPropertyConfig
|
||||||
|
.config as CustomPropertyEnumConfig
|
||||||
|
).values
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'date-cp': {
|
||||||
|
return {
|
||||||
|
subfieldsKey: field.name,
|
||||||
|
dataObject: {
|
||||||
|
type: 'date',
|
||||||
|
operators: RANGE_FIELD_OPERATORS,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'timestamp':
|
||||||
|
case 'integer':
|
||||||
|
case 'number': {
|
||||||
|
return {
|
||||||
|
subfieldsKey: field.name,
|
||||||
|
dataObject: {
|
||||||
|
type: 'number',
|
||||||
|
operators: RANGE_FIELD_OPERATORS,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
subfieldsKey: field.name,
|
||||||
|
dataObject: {
|
||||||
|
type: 'text',
|
||||||
|
valueSources: ['value'],
|
||||||
|
operators: TEXT_FIELD_OPERATORS,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const advancedSearchClassBase = new AdvancedSearchClassBase();
|
const advancedSearchClassBase = new AdvancedSearchClassBase();
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
import Icon, { CloseCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
import Icon, { CloseCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
import { Button, Checkbox, MenuProps, Space, Typography } from 'antd';
|
import { Button, Checkbox, MenuProps, Space, Typography } from 'antd';
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import { isArray, isEmpty } from 'lodash';
|
import { isArray, isEmpty, toLower } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
AsyncFetchListValues,
|
AsyncFetchListValues,
|
||||||
@ -452,3 +452,25 @@ export const getTreeConfig = ({
|
|||||||
? advancedSearchClassBase.getQbConfigs(tierOptions, index, isExplorePage)
|
? advancedSearchClassBase.getQbConfigs(tierOptions, index, isExplorePage)
|
||||||
: jsonLogicSearchClassBase.getQbConfigs(tierOptions, index, isExplorePage);
|
: jsonLogicSearchClassBase.getQbConfigs(tierOptions, index, isExplorePage);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatQueryValueBasedOnType = (
|
||||||
|
value: string[],
|
||||||
|
field: string,
|
||||||
|
type: string
|
||||||
|
) => {
|
||||||
|
if (field.includes('extension') && type === 'text') {
|
||||||
|
return value.map((item) => toLower(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCustomPropertyAdvanceSearchEnumOptions = (
|
||||||
|
enumValues: string[]
|
||||||
|
) => {
|
||||||
|
return enumValues.reduce((acc: Record<string, string>, value) => {
|
||||||
|
acc[value] = value;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import { defaultConjunction } from 'react-awesome-query-builder/lib/utils/defaultUtils';
|
import { defaultConjunction } from 'react-awesome-query-builder/lib/utils/defaultUtils';
|
||||||
import { getWidgetForFieldOp } from 'react-awesome-query-builder/lib/utils/ruleUtils';
|
import { getWidgetForFieldOp } from 'react-awesome-query-builder/lib/utils/ruleUtils';
|
||||||
|
import { formatQueryValueBasedOnType } from './AdvancedSearchUtils';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This script is a modified version of https://github.com/ukrbublik/react-awesome-query-builder/blob/5.1.2/modules/export/elasticSearch.js
|
* This script is a modified version of https://github.com/ukrbublik/react-awesome-query-builder/blob/5.1.2/modules/export/elasticSearch.js
|
||||||
@ -372,7 +373,13 @@ export function elasticSearchFormat(tree, config) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return buildEsRule(field, value, operator, config, valueSrc);
|
return buildEsRule(
|
||||||
|
field,
|
||||||
|
formatQueryValueBasedOnType(value, field, _valueType),
|
||||||
|
operator,
|
||||||
|
config,
|
||||||
|
valueSrc
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user