Fix #21414: Custom properties display issue in advanced search (#21873)

* fix: remove recursive extraction of nested fields for entityReference and entityReferenceList type custom properties

* get displayName field along with other fields in /customProperties api

* show display name instead of name for custom props

* show display name instead of name for custom props

* fix extension type and add tests

* fix tests

* fix data model tests

---------

Co-authored-by: karanh37 <karanh37@gmail.com>
Co-authored-by: Karan Hotchandani <33024356+karanh37@users.noreply.github.com>
This commit is contained in:
sonika-shah 2025-06-26 19:35:45 +05:30 committed by GitHub
parent 94cf3e0fd6
commit 971225dbce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 323 additions and 115 deletions

View File

@ -18,7 +18,14 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
import org.everit.json.schema.*;
import org.everit.json.schema.ArraySchema;
import org.everit.json.schema.BooleanSchema;
import org.everit.json.schema.NullSchema;
import org.everit.json.schema.NumberSchema;
import org.everit.json.schema.ObjectSchema;
import org.everit.json.schema.ReferenceSchema;
import org.everit.json.schema.Schema;
import org.everit.json.schema.StringSchema;
import org.everit.json.schema.loader.SchemaClient;
import org.everit.json.schema.loader.SchemaLoader;
import org.json.JSONObject;
@ -207,7 +214,7 @@ public class SchemaFieldExtractor {
} else {
String fieldType = mapSchemaTypeToSimpleType(fieldSchema);
fieldTypesMap.putIfAbsent(
fullFieldName, new FieldDefinition(fullFieldName, fieldType, null));
fullFieldName, FieldDefinition.of(fullFieldName, fullFieldName, fieldType, null));
processedFields.add(fullFieldName);
LOG.debug("Added field '{}', Type: '{}'", fullFieldName, fieldType);
// Recursively process nested objects or arrays
@ -221,7 +228,8 @@ public class SchemaFieldExtractor {
handleArraySchema(arraySchema, parentPath, fieldTypesMap, processingStack, processedFields);
} else {
String fieldType = mapSchemaTypeToSimpleType(schema);
fieldTypesMap.putIfAbsent(parentPath, new FieldDefinition(parentPath, fieldType, null));
fieldTypesMap.putIfAbsent(
parentPath, FieldDefinition.of(parentPath, parentPath, fieldType, null));
LOG.debug("Added field '{}', Type: '{}'", parentPath, fieldType);
}
} finally {
@ -241,7 +249,7 @@ public class SchemaFieldExtractor {
if (referenceType != null) {
fieldTypesMap.putIfAbsent(
fullFieldName, new FieldDefinition(fullFieldName, referenceType, null));
fullFieldName, FieldDefinition.of(fullFieldName, fullFieldName, referenceType, null));
processedFields.add(fullFieldName);
LOG.debug("Added field '{}', Type: '{}'", fullFieldName, referenceType);
if (referenceType.startsWith("array<") && referenceType.endsWith(">")) {
@ -257,7 +265,8 @@ public class SchemaFieldExtractor {
referredSchema, fullFieldName, fieldTypesMap, processingStack, processedFields);
}
} else {
fieldTypesMap.putIfAbsent(fullFieldName, new FieldDefinition(fullFieldName, "object", null));
fieldTypesMap.putIfAbsent(
fullFieldName, FieldDefinition.of(fullFieldName, fullFieldName, "object", null));
processedFields.add(fullFieldName);
LOG.debug("Added field '{}', Type: 'object'", fullFieldName);
extractFieldsFromSchema(
@ -285,7 +294,7 @@ public class SchemaFieldExtractor {
if (itemsReferenceType != null) {
String arrayFieldType = "array<" + itemsReferenceType + ">";
fieldTypesMap.putIfAbsent(
fullFieldName, new FieldDefinition(fullFieldName, arrayFieldType, null));
fullFieldName, FieldDefinition.of(fullFieldName, fullFieldName, arrayFieldType, null));
processedFields.add(fullFieldName);
LOG.debug("Added field '{}', Type: '{}'", fullFieldName, arrayFieldType);
Schema referredItemsSchema = itemsReferenceSchema.getReferredSchema();
@ -296,7 +305,8 @@ public class SchemaFieldExtractor {
}
String arrayType = mapSchemaTypeToSimpleType(itemsSchema);
fieldTypesMap.putIfAbsent(
fullFieldName, new FieldDefinition(fullFieldName, "array<" + arrayType + ">", null));
fullFieldName,
FieldDefinition.of(fullFieldName, fullFieldName, "array<" + arrayType + ">", null));
processedFields.add(fullFieldName);
LOG.debug("Added field '{}', Type: 'array<{}>'", fullFieldName, arrayType);
@ -321,49 +331,31 @@ public class SchemaFieldExtractor {
String propertyName = customProperty.getName();
String propertyType = customProperty.getPropertyType().getName();
String fullFieldName = propertyName; // No parent path for custom properties
String displayName = customProperty.getDisplayName();
LOG.debug("Processing custom property '{}'", fullFieldName);
Object customPropertyConfigObj = customProperty.getCustomPropertyConfig();
if (isEntityReferenceList(propertyType)) {
String referenceType = "array<entityReference>";
FieldDefinition referenceFieldDefinition =
new FieldDefinition(fullFieldName, referenceType, customPropertyConfigObj);
fieldTypesMap.putIfAbsent(fullFieldName, referenceFieldDefinition);
FieldDefinition fieldDef =
FieldDefinition.of(fullFieldName, displayName, referenceType, customPropertyConfigObj);
fieldTypesMap.putIfAbsent(fullFieldName, fieldDef);
processedFields.add(fullFieldName);
LOG.debug("Added custom property '{}', Type: '{}'", fullFieldName, referenceType);
Schema itemSchema = resolveSchemaByType("entityReference", schemaUri, schemaClient);
if (itemSchema != null) {
extractFieldsFromSchema(
itemSchema, fullFieldName, fieldTypesMap, processingStack, processedFields);
} else {
LOG.warn(
"Schema for type 'entityReference' not found. Skipping nested field extraction for '{}'.",
fullFieldName);
}
} else if (isEntityReference(propertyType)) {
String referenceType = "entityReference";
FieldDefinition referenceFieldDefinition =
new FieldDefinition(fullFieldName, referenceType, customPropertyConfigObj);
fieldTypesMap.putIfAbsent(fullFieldName, referenceFieldDefinition);
FieldDefinition fieldDef =
FieldDefinition.of(fullFieldName, displayName, referenceType, customPropertyConfigObj);
fieldTypesMap.putIfAbsent(fullFieldName, fieldDef);
processedFields.add(fullFieldName);
LOG.debug("Added custom property '{}', Type: '{}'", fullFieldName, referenceType);
Schema referredSchema = resolveSchemaByType("entityReference", schemaUri, schemaClient);
if (referredSchema != null) {
extractFieldsFromSchema(
referredSchema, fullFieldName, fieldTypesMap, processingStack, processedFields);
} else {
LOG.warn(
"Schema for type 'entityReference' not found. Skipping nested field extraction for '{}'.",
fullFieldName);
}
} else {
FieldDefinition entityFieldDefinition =
new FieldDefinition(fullFieldName, propertyType, customPropertyConfigObj);
fieldTypesMap.putIfAbsent(fullFieldName, entityFieldDefinition);
FieldDefinition fieldDef =
FieldDefinition.of(fullFieldName, displayName, propertyType, customPropertyConfigObj);
fieldTypesMap.putIfAbsent(fullFieldName, fieldDef);
processedFields.add(fullFieldName);
LOG.debug("Added custom property '{}', Type: '{}'", fullFieldName, propertyType);
}
@ -375,8 +367,11 @@ public class SchemaFieldExtractor {
for (Map.Entry<String, FieldDefinition> entry : fieldTypesMap.entrySet()) {
FieldDefinition fieldDef = entry.getValue();
fieldsList.add(
new FieldDefinition(
fieldDef.getName(), fieldDef.getType(), fieldDef.getCustomPropertyConfig()));
FieldDefinition.of(
fieldDef.getName(),
fieldDef.getDisplayName(),
fieldDef.getType(),
fieldDef.getCustomPropertyConfig()));
}
return fieldsList;
}
@ -638,13 +633,28 @@ public class SchemaFieldExtractor {
@lombok.Setter
public static class FieldDefinition {
private String name;
private String displayName;
private String type;
private Object customPropertyConfig;
public FieldDefinition(String name, String type, Object customPropertyConfig) {
this.name = name;
this.displayName = name;
this.type = type;
this.customPropertyConfig = customPropertyConfig;
}
public FieldDefinition(
String name, String displayName, String type, Object customPropertyConfig) {
this.name = name;
this.displayName = displayName;
this.type = type;
this.customPropertyConfig = customPropertyConfig;
}
public static FieldDefinition of(
String name, String displayName, String type, Object customPropertyConfig) {
return new FieldDefinition(name, displayName, type, customPropertyConfig);
}
}
}

View File

@ -351,7 +351,7 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
},
},
entity_dashboardDataModel: {
name: 'dataModel',
name: 'dashboardDataModel',
description: 'This is Data Model custom property',
integerValue: '23',
stringValue: 'This is string propery',

View File

@ -100,6 +100,12 @@ test.describe('Advanced Search Custom Property', () => {
'Custom Properties'
);
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
'Table'
);
// Perform click on custom property type to filter
await selectOption(
page,

View File

@ -15,6 +15,7 @@ import { CUSTOM_PROPERTIES_ENTITIES } from '../../../constant/customProperty';
import { GlobalSettingOptions } from '../../../constant/settings';
import { SidebarItem } from '../../../constant/sidebar';
import { DashboardClass } from '../../../support/entity/DashboardClass';
import { selectOption } from '../../../utils/advancedSearch';
import { createNewPage, redirectToHomePage, uuid } from '../../../utils/common';
import {
addCustomPropertiesForEntity,
@ -122,8 +123,16 @@ test('CustomProperty Dashboard Filter', async ({ page }) => {
.getByText('Owner')
.click();
const ruleLocator = page.locator('.rule').nth(0);
await page.getByTitle('Custom Properties').click();
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
'Dashboard'
);
// Select Custom Property Field when we want filter
await page
.locator(
@ -145,13 +154,15 @@ test('CustomProperty Dashboard Filter', async ({ page }) => {
// Validate if filter dashboard appeared
expect(page.getByTestId('advance-search-filter-text')).toContainText(
`extension.${propertyName} = '${propertyValue}'`
await expect(
page.getByTestId('advance-search-filter-text')
).toContainText(
`extension.dashboard.${propertyName} = '${propertyValue}'`
);
expect(page.getByTestId('entity-header-display-name')).toContainText(
dashboardEntity.entity.displayName
);
await expect(
page.getByTestId('entity-header-display-name')
).toContainText(dashboardEntity.entity.displayName);
}
);

View File

@ -17,6 +17,7 @@ import {
addCustomPropertiesForEntity,
deleteCreatedProperty,
editCreatedProperty,
verifyCustomPropertyInAdvancedSearch,
} from '../../utils/customProperty';
import { settingClick, SettingOptionsType } from '../../utils/sidebar';
@ -65,6 +66,18 @@ test.describe('Custom properties without custom property config', () => {
await editCreatedProperty(page, propertyName);
await verifyCustomPropertyInAdvancedSearch(
page,
propertyName.toUpperCase(), // displayName is in uppercase
entity.name.charAt(0).toUpperCase() + entity.name.slice(1)
);
await settingClick(
page,
entity.entityApiType as SettingOptionsType,
true
);
await deleteCreatedProperty(page, propertyName);
});
});

View File

@ -17,6 +17,7 @@ import {
addCustomPropertiesForEntity,
deleteCreatedProperty,
editCreatedProperty,
verifyCustomPropertyInAdvancedSearch,
} from '../../utils/customProperty';
import { settingClick, SettingOptionsType } from '../../utils/sidebar';
@ -51,6 +52,18 @@ test.describe('Custom properties with custom property config', () => {
await editCreatedProperty(page, propertyName, 'Enum');
await verifyCustomPropertyInAdvancedSearch(
page,
propertyName.toUpperCase(), // displayName is in uppercase
entity.name.charAt(0).toUpperCase() + entity.name.slice(1)
);
await settingClick(
page,
entity.entityApiType as SettingOptionsType,
true
);
await deleteCreatedProperty(page, propertyName);
});
});
@ -79,6 +92,18 @@ test.describe('Custom properties with custom property config', () => {
await editCreatedProperty(page, propertyName, 'Table');
await verifyCustomPropertyInAdvancedSearch(
page,
propertyName.toUpperCase(), // displayName is in uppercase
entity.name.charAt(0).toUpperCase() + entity.name.slice(1)
);
await settingClick(
page,
entity.entityApiType as SettingOptionsType,
true
);
await deleteCreatedProperty(page, propertyName);
});
});
@ -111,6 +136,18 @@ test.describe('Custom properties with custom property config', () => {
await editCreatedProperty(page, propertyName, 'Entity Reference');
await verifyCustomPropertyInAdvancedSearch(
page,
propertyName.toUpperCase(), // displayName is in uppercase
entity.name.charAt(0).toUpperCase() + entity.name.slice(1)
);
await settingClick(
page,
entity.entityApiType as SettingOptionsType,
true
);
await deleteCreatedProperty(page, propertyName);
});
});
@ -148,6 +185,18 @@ test.describe('Custom properties with custom property config', () => {
'Entity Reference List'
);
await verifyCustomPropertyInAdvancedSearch(
page,
propertyName.toUpperCase(), // displayName is in uppercase
entity.name.charAt(0).toUpperCase() + entity.name.slice(1)
);
await settingClick(
page,
entity.entityApiType as SettingOptionsType,
true
);
await deleteCreatedProperty(page, propertyName);
});
});
@ -205,6 +254,18 @@ test.describe('Custom properties with custom property config', () => {
await editCreatedProperty(page, propertyName);
await verifyCustomPropertyInAdvancedSearch(
page,
propertyName.toUpperCase(), // displayName is in uppercase
entity.name.charAt(0).toUpperCase() + entity.name.slice(1)
);
await settingClick(
page,
entity.entityApiType as SettingOptionsType,
true
);
await deleteCreatedProperty(page, propertyName);
});
});
@ -235,6 +296,18 @@ test.describe('Custom properties with custom property config', () => {
await editCreatedProperty(page, propertyName);
await verifyCustomPropertyInAdvancedSearch(
page,
propertyName.toUpperCase(), // displayName is in uppercase
entity.name.charAt(0).toUpperCase() + entity.name.slice(1)
);
await settingClick(
page,
entity.entityApiType as SettingOptionsType,
true
);
await deleteCreatedProperty(page, propertyName);
});
});

View File

@ -16,17 +16,20 @@ import {
CUSTOM_PROPERTY_NAME_VALIDATION_ERROR,
ENTITY_REFERENCE_PROPERTIES,
} from '../constant/customProperty';
import { SidebarItem } from '../constant/sidebar';
import {
EntityTypeEndpoint,
ENTITY_PATH,
} from '../support/entity/Entity.interface';
import { UserClass } from '../support/user/UserClass';
import { selectOption, showAdvancedSearchDialog } from './advancedSearch';
import {
clickOutside,
descriptionBox,
descriptionBoxReadOnly,
uuid,
} from './common';
import { sidebarClick } from './sidebar';
export enum CustomPropertyType {
STRING = 'String',
@ -732,7 +735,7 @@ export const editCreatedProperty = async (
// displayName
await page.fill('[data-testid="display-name"]', '');
await page.fill('[data-testid="display-name"]', propertyName);
await page.fill('[data-testid="display-name"]', propertyName.toUpperCase());
await page.locator(descriptionBox).fill('');
await page.locator(descriptionBox).fill('This is new description');
@ -807,3 +810,38 @@ export const deleteCreatedProperty = async (
await page.locator('[data-testid="save-button"]').click();
};
export const verifyCustomPropertyInAdvancedSearch = async (
page: Page,
propertyName: string,
entityType: string
) => {
await sidebarClick(page, SidebarItem.EXPLORE);
await page.waitForLoadState('networkidle');
// Open advanced search dialog
await showAdvancedSearchDialog(page);
const ruleLocator = page.locator('.rule').nth(0);
// Select "Custom Properties" from the field dropdown
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
'Custom Properties'
);
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
entityType
);
await selectOption(
page,
ruleLocator.locator('.rule--field .ant-select'),
propertyName
);
await page.getByTestId('cancel-btn').click();
};

View File

@ -224,18 +224,31 @@ export const AdvanceSearchProvider = ({
try {
const res = await getAllCustomProperties();
Object.entries(res).forEach(([_, fields]) => {
Object.entries(res).forEach(([entityType, fields]) => {
if (Array.isArray(fields) && fields.length > 0) {
// Create nested subfields for each entity type (e.g., table, database, etc.)
const entitySubfields: Record<string, Field> = {};
fields.forEach((field) => {
if (field.name && field.type) {
const { subfieldsKey, dataObject } =
advancedSearchClassBase.getCustomPropertiesSubFields(field);
subfields[subfieldsKey] = {
entitySubfields[subfieldsKey] = {
...dataObject,
valueSources: dataObject.valueSources as ValueSource[],
};
}
});
// Only create the entity type field if it has custom properties
if (!isEmpty(entitySubfields)) {
subfields[entityType] = {
label: entityType.charAt(0).toUpperCase() + entityType.slice(1),
type: '!group',
subfields: entitySubfields,
} as Field;
}
}
});
} catch (error) {

View File

@ -15,6 +15,7 @@
.group.rule_group {
border: none !important;
padding: 0;
.group--children {
padding-top: 0;
padding-bottom: 0;
@ -24,12 +25,20 @@
.group--field {
width: 180px;
.ant-select {
width: 100% !important;
}
label {
font-weight: normal;
margin-bottom: 6px;
}
}
.rule_group {
.group--field {
align-self: flex-start;
}
}
}

View File

@ -44,6 +44,7 @@ import {
getCustomPropertyAdvanceSearchEnumOptions,
renderAdvanceSearchButtons,
} from './AdvancedSearchUtils';
import { getEntityName } from './EntityUtils';
import { getCombinedQueryFilterObject } from './ExplorePage/ExplorePageUtils';
import { renderQueryBuilderFilterButtons } from './QueryBuilderUtils';
import { parseBucketsData } from './SearchUtils';
@ -955,76 +956,77 @@ class AdvancedSearchClassBase {
};
public getCustomPropertiesSubFields(field: CustomPropertySummary) {
{
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,
},
const label = getEntityName(field);
switch (field.type) {
case 'array<entityReference>':
case 'entityReference':
return {
subfieldsKey: field.name + `.displayName`,
dataObject: {
type: 'select',
label,
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 'enum':
return {
subfieldsKey: field.name,
dataObject: {
type: 'select',
label,
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,
},
};
case 'date-cp': {
return {
subfieldsKey: field.name,
dataObject: {
type: 'date',
label,
operators: RANGE_FIELD_OPERATORS,
},
};
}
case 'timestamp':
case 'integer':
case 'number': {
return {
subfieldsKey: field.name,
dataObject: {
type: 'number',
label,
operators: RANGE_FIELD_OPERATORS,
},
};
}
default:
return {
subfieldsKey: field.name,
dataObject: {
type: 'text',
label,
valueSources: ['value'],
operators: TEXT_FIELD_OPERATORS,
},
};
}
}
}

View File

@ -244,6 +244,19 @@ function buildParameters(queryType, value, operator, fieldName, config) {
*/
function buildEsRule(fieldName, value, operator, config, valueSrc) {
if (!fieldName || !operator || value == undefined) return undefined; // rule is not fully entered
// Check if field has custom elasticsearch field mapping or handle extension fields
let actualFieldName = fieldName;
let isNestedExtensionField = false;
let entityType = null;
if (fieldName.startsWith('extension.') && fieldName.split('.').length >= 3) {
const parts = fieldName.split('.');
entityType = parts[1];
actualFieldName = `${parts[0]}.${parts.slice(2).join('.')}`;
isNestedExtensionField = true;
}
let op = operator;
let opConfig = config.operators[op];
if (!opConfig) return undefined; // unknown operator
@ -286,15 +299,17 @@ function buildEsRule(fieldName, value, operator, config, valueSrc) {
queryType,
value,
op,
fieldName,
actualFieldName,
config
);
} else {
parameters = buildParameters(queryType, value, op, fieldName, config);
parameters = buildParameters(queryType, value, op, actualFieldName, config);
}
// Build the main query
let mainQuery;
if (not) {
return {
mainQuery = {
bool: {
must_not: {
[queryType]: { ...parameters },
@ -302,10 +317,28 @@ function buildEsRule(fieldName, value, operator, config, valueSrc) {
},
};
} else {
return {
mainQuery = {
[queryType]: { ...parameters },
};
}
// For nested extension fields, combine with entityType filter
if (isNestedExtensionField && entityType) {
return {
bool: {
must: [
mainQuery,
{
term: {
entityType: entityType,
},
},
],
},
};
}
return mainQuery;
}
/**