diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/TypeRegistry.java b/openmetadata-service/src/main/java/org/openmetadata/service/TypeRegistry.java index f92fdc8ca0e..92ec8feef6b 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/TypeRegistry.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/TypeRegistry.java @@ -118,7 +118,8 @@ public class TypeRegistry { } } } - return null; + throw EntityNotFoundException.byMessage( + CatalogExceptionMessage.entityNotFound(Entity.TYPE, String.valueOf(type))); } public static String getCustomPropertyConfig(String entityType, String propertyName) { @@ -128,7 +129,13 @@ public class TypeRegistry { if (property.getName().equals(propertyName) && property.getCustomPropertyConfig() != null && property.getCustomPropertyConfig().getConfig() != null) { - return property.getCustomPropertyConfig().getConfig().toString(); + Object config = property.getCustomPropertyConfig().getConfig(); + if (config instanceof String || config instanceof Integer) { + return config.toString(); // for simple type config return as string + } else { + return JsonUtils.pojoToJson( + config); // for complex object in config return as JSON string + } } } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java index 947d1b49af9..dfd13f11e3c 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java @@ -67,6 +67,7 @@ import static org.openmetadata.service.util.EntityUtil.objectMatch; import static org.openmetadata.service.util.EntityUtil.tagLabelMatch; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.annotations.VisibleForTesting; import com.google.common.cache.CacheBuilder; @@ -100,6 +101,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiPredicate; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import javax.json.JsonPatch; import javax.validation.constraints.NotNull; import javax.ws.rs.core.Response.Status; @@ -145,6 +147,7 @@ import org.openmetadata.schema.type.api.BulkAssets; import org.openmetadata.schema.type.api.BulkOperationResult; import org.openmetadata.schema.type.api.BulkResponse; import org.openmetadata.schema.type.csv.CsvImportResult; +import org.openmetadata.schema.type.customproperties.EnumWithDescriptionsConfig; import org.openmetadata.schema.utils.EntityInterfaceUtil; import org.openmetadata.service.Entity; import org.openmetadata.service.OpenMetadataApplicationConfig; @@ -1454,34 +1457,20 @@ public abstract class EntityRepository { } String customPropertyType = TypeRegistry.getCustomPropertyType(entityType, fieldName); String propertyConfig = TypeRegistry.getCustomPropertyConfig(entityType, fieldName); - DateTimeFormatter formatter = null; try { - if ("date-cp".equals(customPropertyType)) { - DateTimeFormatter inputFormatter = - DateTimeFormatter.ofPattern(Objects.requireNonNull(propertyConfig), Locale.ENGLISH); - - // Parse the input string into a TemporalAccessor - TemporalAccessor date = inputFormatter.parse(fieldValue.textValue()); - - // Create a formatter for the desired output format - DateTimeFormatter outputFormatter = - DateTimeFormatter.ofPattern(propertyConfig, Locale.ENGLISH); - ((ObjectNode) jsonNode).put(fieldName, outputFormatter.format(date)); - } else if ("dateTime-cp".equals(customPropertyType)) { - formatter = DateTimeFormatter.ofPattern(Objects.requireNonNull(propertyConfig)); - LocalDateTime dateTime = LocalDateTime.parse(fieldValue.textValue(), formatter); - ((ObjectNode) jsonNode).put(fieldName, dateTime.format(formatter)); - } else if ("time-cp".equals(customPropertyType)) { - formatter = DateTimeFormatter.ofPattern(Objects.requireNonNull(propertyConfig)); - LocalTime time = LocalTime.parse(fieldValue.textValue(), formatter); - ((ObjectNode) jsonNode).put(fieldName, time.format(formatter)); - } + validateAndUpdateExtensionBasedOnPropertyType( + entity, + (ObjectNode) jsonNode, + fieldName, + fieldValue, + customPropertyType, + propertyConfig); } catch (DateTimeParseException e) { throw new IllegalArgumentException( - CatalogExceptionMessage.dateTimeValidationError( - fieldName, TypeRegistry.getCustomPropertyConfig(entityType, fieldName))); + CatalogExceptionMessage.dateTimeValidationError(fieldName, propertyConfig)); } - Set validationMessages = jsonSchema.validate(fieldValue); + + Set validationMessages = jsonSchema.validate(entry.getValue()); if (!validationMessages.isEmpty()) { throw new IllegalArgumentException( CatalogExceptionMessage.jsonValidationError(fieldName, validationMessages.toString())); @@ -1489,6 +1478,94 @@ public abstract class EntityRepository { } } + private void validateAndUpdateExtensionBasedOnPropertyType( + T entity, + ObjectNode jsonNode, + String fieldName, + JsonNode fieldValue, + String customPropertyType, + String propertyConfig) { + + switch (customPropertyType) { + case "date-cp", "dateTime-cp", "time-cp" -> { + String formattedValue = + getFormattedDateTimeField( + fieldValue.textValue(), customPropertyType, propertyConfig, fieldName); + jsonNode.put(fieldName, formattedValue); + } + case "enumWithDescriptions" -> handleEnumWithDescriptions( + fieldName, fieldValue, propertyConfig, jsonNode, entity); + default -> {} + } + } + + private String getFormattedDateTimeField( + String fieldValue, String customPropertyType, String propertyConfig, String fieldName) { + DateTimeFormatter formatter; + + try { + return switch (customPropertyType) { + case "date-cp" -> { + DateTimeFormatter inputFormatter = + DateTimeFormatter.ofPattern(propertyConfig, Locale.ENGLISH); + TemporalAccessor date = inputFormatter.parse(fieldValue); + DateTimeFormatter outputFormatter = + DateTimeFormatter.ofPattern(propertyConfig, Locale.ENGLISH); + yield outputFormatter.format(date); + } + case "dateTime-cp" -> { + formatter = DateTimeFormatter.ofPattern(propertyConfig); + LocalDateTime dateTime = LocalDateTime.parse(fieldValue, formatter); + yield dateTime.format(formatter); + } + case "time-cp" -> { + formatter = DateTimeFormatter.ofPattern(propertyConfig); + LocalTime time = LocalTime.parse(fieldValue, formatter); + yield time.format(formatter); + } + default -> throw new IllegalArgumentException( + "Unsupported customPropertyType: " + customPropertyType); + }; + } catch (DateTimeParseException e) { + throw new IllegalArgumentException( + CatalogExceptionMessage.dateTimeValidationError(fieldName, propertyConfig)); + } + } + + private void handleEnumWithDescriptions( + String fieldName, JsonNode fieldValue, String propertyConfig, ObjectNode jsonNode, T entity) { + JsonNode propertyConfigNode = JsonUtils.readTree(propertyConfig); + EnumWithDescriptionsConfig config = + JsonUtils.treeToValue(propertyConfigNode, EnumWithDescriptionsConfig.class); + + if (!config.getMultiSelect() && fieldValue.size() > 1) { + throw new IllegalArgumentException( + "Only one key is allowed for non-multiSelect enumWithDescriptions"); + } + // Replace each enumWithDescriptions key in the fieldValue with the corresponding object from + // the propertyConfig + Map keyToObjectMap = + StreamSupport.stream(propertyConfigNode.get("values").spliterator(), false) + .collect(Collectors.toMap(node -> node.get("key").asText(), node -> node)); + + if (fieldValue.isArray()) { + ArrayNode newArray = JsonUtils.getObjectNode().arrayNode(); + fieldValue.forEach( + valueNode -> { + String key = valueNode.isTextual() ? valueNode.asText() : valueNode.get("key").asText(); + JsonNode valueObject = keyToObjectMap.get(key); + + if (valueObject == null) { + throw new IllegalArgumentException("Key not found in propertyConfig: " + key); + } + newArray.add(valueNode.isTextual() ? valueObject : valueNode); + }); + + jsonNode.replace(fieldName, newArray); + entity.setExtension(JsonUtils.treeToValue(jsonNode, Object.class)); + } + } + public final void storeExtension(EntityInterface entity) { JsonNode jsonNode = JsonUtils.valueToTree(entity.getExtension()); Iterator> customFields = jsonNode.fields(); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TypeRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TypeRepository.java index 5f381be4a90..c78250586ad 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TypeRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TypeRepository.java @@ -28,6 +28,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import javax.ws.rs.core.UriInfo; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Triple; @@ -40,6 +41,8 @@ import org.openmetadata.schema.type.EntityReference; import org.openmetadata.schema.type.Include; import org.openmetadata.schema.type.Relationship; import org.openmetadata.schema.type.customproperties.EnumConfig; +import org.openmetadata.schema.type.customproperties.EnumWithDescriptionsConfig; +import org.openmetadata.schema.type.customproperties.Value; import org.openmetadata.service.Entity; import org.openmetadata.service.TypeRegistry; import org.openmetadata.service.resources.types.TypeResource; @@ -169,24 +172,9 @@ public class TypeRepository extends EntityRepository { private void validateProperty(CustomProperty customProperty) { switch (customProperty.getPropertyType().getName()) { - case "enum" -> { - CustomPropertyConfig config = customProperty.getCustomPropertyConfig(); - if (config != null) { - EnumConfig enumConfig = JsonUtils.convertValue(config.getConfig(), EnumConfig.class); - if (enumConfig == null - || (enumConfig.getValues() != null && enumConfig.getValues().isEmpty())) { - throw new IllegalArgumentException( - "Enum Custom Property Type must have EnumConfig populated with values."); - } else if (enumConfig.getValues() != null - && enumConfig.getValues().stream().distinct().count() - != enumConfig.getValues().size()) { - throw new IllegalArgumentException( - "Enum Custom Property values cannot have duplicates."); - } - } else { - throw new IllegalArgumentException("Enum Custom Property Type must have EnumConfig."); - } - } + case "enum" -> validateEnumConfig(customProperty.getCustomPropertyConfig()); + case "enumWithDescriptions" -> validateEnumWithDescriptionsConfig( + customProperty.getCustomPropertyConfig()); case "date-cp" -> validateDateFormat( customProperty.getCustomPropertyConfig(), getDateTokens(), "Invalid date format"); case "dateTime-cp" -> validateDateFormat( @@ -229,6 +217,44 @@ public class TypeRepository extends EntityRepository { return Set.of('H', 'h', 'm', 's', 'a', 'S'); } + private void validateEnumConfig(CustomPropertyConfig config) { + if (config != null) { + EnumConfig enumConfig = JsonUtils.convertValue(config.getConfig(), EnumConfig.class); + if (enumConfig == null + || (enumConfig.getValues() != null && enumConfig.getValues().isEmpty())) { + throw new IllegalArgumentException( + "Enum Custom Property Type must have EnumConfig populated with values."); + } else if (enumConfig.getValues() != null + && enumConfig.getValues().stream().distinct().count() != enumConfig.getValues().size()) { + throw new IllegalArgumentException("Enum Custom Property values cannot have duplicates."); + } + } else { + throw new IllegalArgumentException("Enum Custom Property Type must have EnumConfig."); + } + } + + private void validateEnumWithDescriptionsConfig(CustomPropertyConfig config) { + if (config != null) { + EnumWithDescriptionsConfig enumWithDescriptionsConfig = + JsonUtils.convertValue(config.getConfig(), EnumWithDescriptionsConfig.class); + if (enumWithDescriptionsConfig == null + || (enumWithDescriptionsConfig.getValues() != null + && enumWithDescriptionsConfig.getValues().isEmpty())) { + throw new IllegalArgumentException( + "EnumWithDescriptions Custom Property Type must have customPropertyConfig populated with values."); + } + JsonUtils.validateJsonSchema(config.getConfig(), EnumWithDescriptionsConfig.class); + if (enumWithDescriptionsConfig.getValues().stream().map(Value::getKey).distinct().count() + != enumWithDescriptionsConfig.getValues().size()) { + throw new IllegalArgumentException( + "EnumWithDescriptions Custom Property key cannot have duplicates."); + } + } else { + throw new IllegalArgumentException( + "EnumWithDescriptions Custom Property Type must have customPropertyConfig."); + } + } + /** Handles entity updated from PUT and POST operation. */ public class TypeUpdater extends EntityUpdater { public TypeUpdater(Type original, Type updated, Operation operation) { @@ -387,6 +413,27 @@ public class TypeRepository extends EntityRepository { throw new IllegalArgumentException( "Existing Enum Custom Property values cannot be removed."); } + } else if (origProperty.getPropertyType().getName().equals("enumWithDescriptions")) { + EnumWithDescriptionsConfig origConfig = + JsonUtils.convertValue( + origProperty.getCustomPropertyConfig().getConfig(), + EnumWithDescriptionsConfig.class); + EnumWithDescriptionsConfig updatedConfig = + JsonUtils.convertValue( + updatedProperty.getCustomPropertyConfig().getConfig(), + EnumWithDescriptionsConfig.class); + HashSet updatedValues = + updatedConfig.getValues().stream() + .map(Value::getKey) + .collect(Collectors.toCollection(HashSet::new)); + if (updatedValues.size() != updatedConfig.getValues().size()) { + throw new IllegalArgumentException( + "EnumWithDescriptions Custom Property values cannot have duplicates."); + } else if (!updatedValues.containsAll( + origConfig.getValues().stream().map(Value::getKey).collect(Collectors.toSet()))) { + throw new IllegalArgumentException( + "Existing EnumWithDescriptions Custom Property values cannot be removed."); + } } } } diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java index 842ad0ef01e..bf627e44292 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java @@ -387,6 +387,8 @@ public abstract class EntityResourceTest { INT_TYPE = getEntityByName("integer", "", ADMIN_AUTH_HEADERS); STRING_TYPE = getEntityByName("string", "", ADMIN_AUTH_HEADERS); ENUM_TYPE = getEntityByName("enum", "", ADMIN_AUTH_HEADERS); + ENUM_WITH_DESCRIPTIONS_TYPE = getEntityByName("enumWithDescriptions", "", ADMIN_AUTH_HEADERS); } @Override @@ -280,6 +283,177 @@ public class TypeResourceTest extends EntityResourceTest { new ArrayList<>(List.of(fieldA, fieldB)), tableEntity.getCustomProperties());*/ } + @Test + void put_patch_customProperty_enumWithDescriptions_200() throws IOException { + Type databaseEntity = getEntityByName("database", "customProperties", ADMIN_AUTH_HEADERS); + + // Add a custom property of type enumWithDescriptions with PUT + CustomProperty enumWithDescriptionsFieldA = + new CustomProperty() + .withName("enumWithDescriptionsTest") + .withDescription("enumWithDescriptionsTest") + .withPropertyType(ENUM_WITH_DESCRIPTIONS_TYPE.getEntityReference()); + ChangeDescription change = getChangeDescription(databaseEntity, MINOR_UPDATE); + fieldAdded(change, "customProperties", new ArrayList<>(List.of(enumWithDescriptionsFieldA))); + Type finalDatabaseEntity = databaseEntity; + ChangeDescription finalChange = change; + assertResponseContains( + () -> + addCustomPropertyAndCheck( + finalDatabaseEntity.getId(), + enumWithDescriptionsFieldA, + ADMIN_AUTH_HEADERS, + MINOR_UPDATE, + finalChange), + Status.BAD_REQUEST, + "EnumWithDescriptions Custom Property Type must have customPropertyConfig."); + enumWithDescriptionsFieldA.setCustomPropertyConfig( + new CustomPropertyConfig().withConfig(new EnumWithDescriptionsConfig())); + ChangeDescription change1 = getChangeDescription(databaseEntity, MINOR_UPDATE); + Type databaseEntity1 = databaseEntity; + assertResponseContains( + () -> + addCustomPropertyAndCheck( + databaseEntity1.getId(), + enumWithDescriptionsFieldA, + ADMIN_AUTH_HEADERS, + MINOR_UPDATE, + change1), + Status.BAD_REQUEST, + "EnumWithDescriptions Custom Property Type must have customPropertyConfig populated with values."); + + List valuesWithDuplicateKey = + List.of( + new Value().withKey("A").withDescription("Description A"), + new Value().withKey("B").withDescription("Description B"), + new Value().withKey("C").withDescription("Description C"), + new Value().withKey("C").withDescription("Description C")); + + enumWithDescriptionsFieldA.setCustomPropertyConfig( + new CustomPropertyConfig() + .withConfig(new EnumWithDescriptionsConfig().withValues(valuesWithDuplicateKey))); + ChangeDescription change7 = getChangeDescription(databaseEntity, MINOR_UPDATE); + Type databaseEntity2 = databaseEntity; + assertResponseContains( + () -> + addCustomPropertyAndCheck( + databaseEntity2.getId(), + enumWithDescriptionsFieldA, + ADMIN_AUTH_HEADERS, + MINOR_UPDATE, + change7), + Status.BAD_REQUEST, + "EnumWithDescriptions Custom Property key cannot have duplicates."); + List valuesWithUniqueKey = + List.of( + new Value().withKey("A").withDescription("Description A"), + new Value().withKey("B").withDescription("Description B"), + new Value().withKey("C").withDescription("Description C")); + enumWithDescriptionsFieldA.setCustomPropertyConfig( + new CustomPropertyConfig() + .withConfig(new EnumWithDescriptionsConfig().withValues(valuesWithUniqueKey))); + databaseEntity = + addCustomPropertyAndCheck( + databaseEntity.getId(), + enumWithDescriptionsFieldA, + ADMIN_AUTH_HEADERS, + MINOR_UPDATE, + change); + assertCustomProperties( + new ArrayList<>(List.of(enumWithDescriptionsFieldA)), databaseEntity.getCustomProperties()); + CustomPropertyConfig prevConfig = enumWithDescriptionsFieldA.getCustomPropertyConfig(); + // Changing custom property description with PUT + enumWithDescriptionsFieldA.withDescription("updatedEnumWithDescriptionsTest"); + ChangeDescription change2 = getChangeDescription(databaseEntity, MINOR_UPDATE); + fieldUpdated( + change2, + EntityUtil.getCustomField(enumWithDescriptionsFieldA, "description"), + "enumWithDescriptionsTest", + "updatedEnumWithDescriptionsTest"); + databaseEntity = + addCustomPropertyAndCheck( + databaseEntity.getId(), + enumWithDescriptionsFieldA, + ADMIN_AUTH_HEADERS, + MINOR_UPDATE, + change2); + assertCustomProperties( + new ArrayList<>(List.of(enumWithDescriptionsFieldA)), databaseEntity.getCustomProperties()); + + List valuesWithUniqueKeyAB = + List.of( + new Value().withKey("A").withDescription("Description A"), + new Value().withKey("B").withDescription("Description B")); + enumWithDescriptionsFieldA.setCustomPropertyConfig( + new CustomPropertyConfig() + .withConfig(new EnumWithDescriptionsConfig().withValues(valuesWithUniqueKeyAB))); + ChangeDescription change3 = getChangeDescription(databaseEntity, MINOR_UPDATE); + assertResponseContains( + () -> + addCustomPropertyAndCheck( + databaseEntity1.getId(), + enumWithDescriptionsFieldA, + ADMIN_AUTH_HEADERS, + MINOR_UPDATE, + change3), + Status.BAD_REQUEST, + "Existing EnumWithDescriptions Custom Property values cannot be removed."); + + enumWithDescriptionsFieldA.setCustomPropertyConfig( + new CustomPropertyConfig() + .withConfig(new EnumWithDescriptionsConfig().withValues(valuesWithDuplicateKey))); + ChangeDescription change4 = getChangeDescription(databaseEntity, MINOR_UPDATE); + assertResponseContains( + () -> + addCustomPropertyAndCheck( + databaseEntity1.getId(), + enumWithDescriptionsFieldA, + ADMIN_AUTH_HEADERS, + MINOR_UPDATE, + change4), + Status.BAD_REQUEST, + "EnumWithDescriptions Custom Property key cannot have duplicates."); + valuesWithUniqueKey = + List.of( + new Value().withKey("A").withDescription("Description A"), + new Value().withKey("B").withDescription("Description B"), + new Value().withKey("C").withDescription("Description C"), + new Value().withKey("D").withDescription("Description D")); + ChangeDescription change5 = getChangeDescription(databaseEntity, MINOR_UPDATE); + enumWithDescriptionsFieldA.setCustomPropertyConfig( + new CustomPropertyConfig() + .withConfig(new EnumWithDescriptionsConfig().withValues(valuesWithUniqueKey))); + fieldUpdated( + change5, + EntityUtil.getCustomField(enumWithDescriptionsFieldA, "customPropertyConfig"), + prevConfig, + enumWithDescriptionsFieldA.getCustomPropertyConfig()); + databaseEntity = + addCustomPropertyAndCheck( + databaseEntity.getId(), + enumWithDescriptionsFieldA, + ADMIN_AUTH_HEADERS, + MINOR_UPDATE, + change5); + assertCustomProperties( + new ArrayList<>(List.of(enumWithDescriptionsFieldA)), databaseEntity.getCustomProperties()); + + // Changing custom property description with PATCH + // Changes from this PATCH is consolidated with the previous changes + enumWithDescriptionsFieldA.withDescription("updated2"); + String json = JsonUtils.pojoToJson(databaseEntity); + databaseEntity.setCustomProperties(List.of(enumWithDescriptionsFieldA)); + change = getChangeDescription(databaseEntity, CHANGE_CONSOLIDATED); + fieldUpdated( + change5, + EntityUtil.getCustomField(enumWithDescriptionsFieldA, "description"), + "updatedEnumWithDescriptionsTest", + "updated2"); + + databaseEntity = + patchEntityAndCheck(databaseEntity, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change5); + } + @Test void put_customPropertyToPropertyType_4xx() { // Adding a custom property to a property type is not allowed (only entity type is allowed) diff --git a/openmetadata-spec/src/main/resources/json/schema/type/basic.json b/openmetadata-spec/src/main/resources/json/schema/type/basic.json index 4c2528391da..a96aa49df4d 100644 --- a/openmetadata-spec/src/main/resources/json/schema/type/basic.json +++ b/openmetadata-spec/src/main/resources/json/schema/type/basic.json @@ -105,6 +105,22 @@ "type": "string" } }, + "enumWithDescriptions": { + "$comment": "@om-field-type", + "description": "List of values in Enum with description for each key.", + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "description": { + "type": "string" + } + } + } + }, "timezone": { "description": "Timezone of the user in the format `America/Los_Angeles`, `Brazil/East`, etc.", "type": "string", diff --git a/openmetadata-spec/src/main/resources/json/schema/type/customProperties/enumWithDescriptionsConfig.json b/openmetadata-spec/src/main/resources/json/schema/type/customProperties/enumWithDescriptionsConfig.json new file mode 100644 index 00000000000..e241a4b7555 --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/type/customProperties/enumWithDescriptionsConfig.json @@ -0,0 +1,31 @@ +{ + "$id": "https://open-metadata.org/schema/type/customPropertyEnumWithDescriptionsConfig.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EnumWithDescriptionsConfig", + "type": "object", + "javaType": "org.openmetadata.schema.type.customproperties.EnumWithDescriptionsConfig", + "description": "Custom property to define enum values with descriptions and extendable fields", + "properties": { + "multiSelect": { + "type": "boolean", + "default": false + }, + "values": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": ["key","description"], + "additionalProperties": false + } + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/openmetadata-spec/src/main/resources/json/schema/type/customProperty.json b/openmetadata-spec/src/main/resources/json/schema/type/customProperty.json index 4a55e562dbf..4073605c01d 100644 --- a/openmetadata-spec/src/main/resources/json/schema/type/customProperty.json +++ b/openmetadata-spec/src/main/resources/json/schema/type/customProperty.json @@ -33,6 +33,9 @@ }, { "$ref": "#/definitions/entityTypes" + }, + { + "$ref": "../type/customProperties/enumWithDescriptionsConfig.json" } ] } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/constant/customProperty.ts b/openmetadata-ui/src/main/resources/ui/playwright/constant/customProperty.ts index ffd09515721..53aa7838853 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/constant/customProperty.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/constant/customProperty.ts @@ -47,6 +47,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = { values: ['enum1', 'enum2', 'enum3'], multiSelect: false, }, + enumWithDescriptionConfig: { + values: [ + { + key: 'enumWithDescription1', + description: 'This is enumWithDescription1', + }, + { + key: 'enumWithDescription2', + description: 'This is enumWithDescription2', + }, + ], + multiSelect: false, + }, dateFormatConfig: 'yyyy-MM-dd', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', timeFormatConfig: 'HH:mm:ss', @@ -65,6 +78,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = { values: ['enum1', 'enum2', 'enum3'], multiSelect: false, }, + enumWithDescriptionConfig: { + values: [ + { + key: 'enumWithDescription1', + description: 'This is enumWithDescription1', + }, + { + key: 'enumWithDescription2', + description: 'This is enumWithDescription2', + }, + ], + multiSelect: false, + }, dateFormatConfig: 'yyyy-MM-dd', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', timeFormatConfig: 'HH:mm:ss', @@ -83,6 +109,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = { values: ['enum1', 'enum2', 'enum3'], multiSelect: false, }, + enumWithDescriptionConfig: { + values: [ + { + key: 'enumWithDescription1', + description: 'This is enumWithDescription1', + }, + { + key: 'enumWithDescription2', + description: 'This is enumWithDescription2', + }, + ], + multiSelect: false, + }, dateFormatConfig: 'yyyy-MM-dd', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', timeFormatConfig: 'HH:mm:ss', @@ -101,6 +140,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = { values: ['enum1', 'enum2', 'enum3'], multiSelect: false, }, + enumWithDescriptionConfig: { + values: [ + { + key: 'enumWithDescription1', + description: 'This is enumWithDescription1', + }, + { + key: 'enumWithDescription2', + description: 'This is enumWithDescription2', + }, + ], + multiSelect: false, + }, dateFormatConfig: 'yyyy-MM-dd', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', timeFormatConfig: 'HH:mm:ss', @@ -119,6 +171,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = { values: ['enum1', 'enum2', 'enum3'], multiSelect: false, }, + enumWithDescriptionConfig: { + values: [ + { + key: 'enumWithDescription1', + description: 'This is enumWithDescription1', + }, + { + key: 'enumWithDescription2', + description: 'This is enumWithDescription2', + }, + ], + multiSelect: false, + }, dateFormatConfig: 'yyyy-MM-dd', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', timeFormatConfig: 'HH:mm:ss', @@ -137,6 +202,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = { values: ['enum1', 'enum2', 'enum3'], multiSelect: false, }, + enumWithDescriptionConfig: { + values: [ + { + key: 'enumWithDescription1', + description: 'This is enumWithDescription1', + }, + { + key: 'enumWithDescription2', + description: 'This is enumWithDescription2', + }, + ], + multiSelect: false, + }, dateFormatConfig: 'yyyy-MM-dd', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', timeFormatConfig: 'HH:mm:ss', @@ -155,6 +233,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = { values: ['enum1', 'enum2', 'enum3'], multiSelect: true, }, + enumWithDescriptionConfig: { + values: [ + { + key: 'enumWithDescription1', + description: 'This is enumWithDescription1', + }, + { + key: 'enumWithDescription2', + description: 'This is enumWithDescription2', + }, + ], + multiSelect: false, + }, dateFormatConfig: 'yyyy-MM-dd', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', timeFormatConfig: 'HH:mm:ss', @@ -173,6 +264,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = { values: ['enum1', 'enum2', 'enum3'], multiSelect: false, }, + enumWithDescriptionConfig: { + values: [ + { + key: 'enumWithDescription1', + description: 'This is enumWithDescription1', + }, + { + key: 'enumWithDescription2', + description: 'This is enumWithDescription2', + }, + ], + multiSelect: false, + }, dateFormatConfig: 'yyyy-MM-dd', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', timeFormatConfig: 'HH:mm:ss', @@ -191,6 +295,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = { values: ['enum1', 'enum2', 'enum3'], multiSelect: false, }, + enumWithDescriptionConfig: { + values: [ + { + key: 'enumWithDescription1', + description: 'This is enumWithDescription1', + }, + { + key: 'enumWithDescription2', + description: 'This is enumWithDescription2', + }, + ], + multiSelect: false, + }, dateFormatConfig: 'yyyy-MM-dd', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', timeFormatConfig: 'HH:mm:ss', @@ -209,6 +326,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = { values: ['enum1', 'enum2', 'enum3'], multiSelect: false, }, + enumWithDescriptionConfig: { + values: [ + { + key: 'enumWithDescription1', + description: 'This is enumWithDescription1', + }, + { + key: 'enumWithDescription2', + description: 'This is enumWithDescription2', + }, + ], + multiSelect: false, + }, dateFormatConfig: 'yyyy-MM-dd', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', timeFormatConfig: 'HH:mm:ss', @@ -227,6 +357,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = { values: ['enum1', 'enum2', 'enum3'], multiSelect: false, }, + enumWithDescriptionConfig: { + values: [ + { + key: 'enumWithDescription1', + description: 'This is enumWithDescription1', + }, + { + key: 'enumWithDescription2', + description: 'This is enumWithDescription2', + }, + ], + multiSelect: false, + }, dateFormatConfig: 'yyyy-MM-dd', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', timeFormatConfig: 'HH:mm:ss', @@ -244,6 +387,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = { values: ['enum1', 'enum2', 'enum3'], multiSelect: false, }, + enumWithDescriptionConfig: { + values: [ + { + key: 'enumWithDescription1', + description: 'This is enumWithDescription1', + }, + { + key: 'enumWithDescription2', + description: 'This is enumWithDescription2', + }, + ], + multiSelect: false, + }, dateFormatConfig: 'yyyy-MM-dd', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', timeFormatConfig: 'HH:mm:ss', @@ -262,6 +418,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = { values: ['enum1', 'enum2', 'enum3'], multiSelect: false, }, + enumWithDescriptionConfig: { + values: [ + { + key: 'enumWithDescription1', + description: 'This is enumWithDescription1', + }, + { + key: 'enumWithDescription2', + description: 'This is enumWithDescription2', + }, + ], + multiSelect: false, + }, dateFormatConfig: 'yyyy-MM-dd', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', timeFormatConfig: 'HH:mm:ss', @@ -279,6 +448,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = { values: ['enum1', 'enum2', 'enum3'], multiSelect: false, }, + enumWithDescriptionConfig: { + values: [ + { + key: 'enumWithDescription1', + description: 'This is enumWithDescription1', + }, + { + key: 'enumWithDescription2', + description: 'This is enumWithDescription2', + }, + ], + multiSelect: false, + }, dateFormatConfig: 'yyyy-MM-dd', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', timeFormatConfig: 'HH:mm:ss', @@ -296,6 +478,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = { values: ['enum1', 'enum2', 'enum3'], multiSelect: false, }, + enumWithDescriptionConfig: { + values: [ + { + key: 'enumWithDescription1', + description: 'This is enumWithDescription1', + }, + { + key: 'enumWithDescription2', + description: 'This is enumWithDescription2', + }, + ], + multiSelect: false, + }, dateFormatConfig: 'yyyy-MM-dd', dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss', timeFormatConfig: 'HH:mm:ss', diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/AdvanceSearchFilter/CustomPropertyAdvanceSeach.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/AdvanceSearchFilter/CustomPropertyAdvanceSeach.spec.ts index d051cb25f30..6f509f52950 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/AdvanceSearchFilter/CustomPropertyAdvanceSeach.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/AdvanceSearchFilter/CustomPropertyAdvanceSeach.spec.ts @@ -60,12 +60,24 @@ test('CustomProperty Dashboard Filter', async ({ page }) => { await test.step('Add Custom Property in Dashboard', async () => { await dashboardEntity.visitEntityPage(page); + const container = page.locator( + `[data-testid="custom-property-${propertyName}-card"]` + ); + await page.getByTestId('custom_properties').click(); - await page - .getByRole('row', { name: `${propertyName} No data` }) - .locator('svg') - .click(); + await expect( + page.locator( + `[data-testid="custom-property-${propertyName}-card"] [data-testid="property-name"]` + ) + ).toHaveText(propertyName); + + const editButton = page.locator( + `[data-testid="custom-property-${propertyName}-card"] [data-testid="edit-icon"]` + ); + + await editButton.scrollIntoViewIfNeeded(); + await editButton.click({ force: true }); await page.getByTestId('value-input').fill(propertyValue); @@ -75,9 +87,7 @@ test('CustomProperty Dashboard Filter', async ({ page }) => { await saveResponse; - expect( - page.getByLabel('Custom Properties').getByTestId('value') - ).toContainText(propertyValue); + await expect(container.getByTestId('value')).toContainText(propertyValue); }); await test.step( diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Customproperties-part2.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Customproperties-part2.spec.ts index 48be7ecd4be..c75d77638af 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Customproperties-part2.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Customproperties-part2.spec.ts @@ -45,13 +45,46 @@ test.describe('Custom properties with custom property config', () => { enumConfig: entity.enumConfig, }); - await editCreatedProperty(page, propertyName); + await editCreatedProperty(page, propertyName, 'Enum'); await deleteCreatedProperty(page, propertyName); }); }); }); + test.describe( + 'Add update and delete Enum With Descriptions custom properties', + () => { + Object.values(CUSTOM_PROPERTIES_ENTITIES).forEach(async (entity) => { + const propertyName = `pwcustomproperty${entity.name}test${uuid()}`; + + test(`Add Enum With Descriptions custom property for ${entity.name}`, async ({ + page, + }) => { + test.slow(true); + + await settingClick(page, entity.entityApiType, true); + + await addCustomPropertiesForEntity({ + page, + propertyName, + customPropertyData: entity, + customType: 'Enum With Descriptions', + enumWithDescriptionConfig: entity.enumWithDescriptionConfig, + }); + + await editCreatedProperty( + page, + propertyName, + 'Enum With Descriptions' + ); + + await deleteCreatedProperty(page, propertyName); + }); + }); + } + ); + test.describe( 'Add update and delete Entity Reference custom properties', () => { @@ -73,7 +106,7 @@ test.describe('Custom properties with custom property config', () => { entityReferenceConfig: entity.entityReferenceConfig, }); - await editCreatedProperty(page, propertyName); + await editCreatedProperty(page, propertyName, 'Entity Reference'); await deleteCreatedProperty(page, propertyName); }); @@ -102,7 +135,11 @@ test.describe('Custom properties with custom property config', () => { entityReferenceConfig: entity.entityReferenceConfig, }); - await editCreatedProperty(page, propertyName); + await editCreatedProperty( + page, + propertyName, + 'Entity Reference List' + ); await deleteCreatedProperty(page, propertyName); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts index 3cb8491d568..ddc63ee9a8a 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts @@ -44,6 +44,7 @@ export enum CustomPropertyTypeByName { TIME_CP = 'time-cp', DATE_CP = 'date-cp', DATE_TIME_CP = 'dateTime-cp', + ENUM_WITH_DESCRIPTION = 'enumWithDescriptions', } export interface CustomProperty { @@ -66,12 +67,18 @@ export const setValueForProperty = async (data: { const { page, propertyName, value, propertyType, endpoint } = data; await page.click('[data-testid="custom_properties"]'); - await expect(page.getByRole('cell', { name: propertyName })).toContainText( - propertyName + const container = page.locator( + `[data-testid="custom-property-${propertyName}-card"]` ); + await expect( + page.locator( + `[data-testid="custom-property-${propertyName}-card"] [data-testid="property-name"]` + ) + ).toHaveText(propertyName); + const editButton = page.locator( - `[data-row-key="${propertyName}"] [data-testid="edit-icon"]` + `[data-testid="custom-property-${propertyName}-card"] [data-testid="edit-icon"]` ); await editButton.scrollIntoViewIfNeeded(); await editButton.click({ force: true }); @@ -96,14 +103,14 @@ export const setValueForProperty = async (data: { case 'email': await page.locator('[data-testid="email-input"]').isVisible(); await page.locator('[data-testid="email-input"]').fill(value); - await page.locator('[data-testid="inline-save-btn"]').click(); + await container.locator('[data-testid="inline-save-btn"]').click(); break; case 'duration': await page.locator('[data-testid="duration-input"]').isVisible(); await page.locator('[data-testid="duration-input"]').fill(value); - await page.locator('[data-testid="inline-save-btn"]').click(); + await container.locator('[data-testid="inline-save-btn"]').click(); break; @@ -112,21 +119,30 @@ export const setValueForProperty = async (data: { await page.fill('#enumValues', value); await page.press('#enumValues', 'Enter'); await clickOutside(page); - await page.click('[data-testid="inline-save-btn"]'); + await container.locator('[data-testid="inline-save-btn"]').click(); + + break; + + case 'enumWithDescriptions': + await page.click('#enumWithDescriptionValues'); + await page.fill('#enumWithDescriptionValues', value, { force: true }); + await page.press('#enumWithDescriptionValues', 'Enter'); + await clickOutside(page); + await container.locator('[data-testid="inline-save-btn"]').click(); break; case 'sqlQuery': await page.locator("pre[role='presentation']").last().click(); await page.keyboard.type(value); - await page.locator('[data-testid="inline-save-btn"]').click(); + await container.locator('[data-testid="inline-save-btn"]').click(); break; case 'timestamp': await page.locator('[data-testid="timestamp-input"]').isVisible(); await page.locator('[data-testid="timestamp-input"]').fill(value); - await page.locator('[data-testid="inline-save-btn"]').click(); + await container.locator('[data-testid="inline-save-btn"]').click(); break; @@ -136,7 +152,7 @@ export const setValueForProperty = async (data: { await page.locator('[data-testid="start-input"]').fill(startValue); await page.locator('[data-testid="end-input"]').isVisible(); await page.locator('[data-testid="end-input"]').fill(endValue); - await page.locator('[data-testid="inline-save-btn"]').click(); + await container.locator('[data-testid="inline-save-btn"]').click(); break; } @@ -146,7 +162,7 @@ export const setValueForProperty = async (data: { await page.locator('[data-testid="time-picker"]').click(); await page.locator('[data-testid="time-picker"]').fill(value); await page.getByRole('button', { name: 'OK', exact: true }).click(); - await page.locator('[data-testid="inline-save-btn"]').click(); + await container.locator('[data-testid="inline-save-btn"]').click(); break; } @@ -161,7 +177,7 @@ export const setValueForProperty = async (data: { } else { await page.getByText('Today', { exact: true }).click(); } - await page.locator('[data-testid="inline-save-btn"]').click(); + await container.locator('[data-testid="inline-save-btn"]').click(); break; } @@ -171,7 +187,7 @@ export const setValueForProperty = async (data: { case 'number': await page.locator('[data-testid="value-input"]').isVisible(); await page.locator('[data-testid="value-input"]').fill(value); - await page.locator('[data-testid="inline-save-btn"]').click(); + await container.locator('[data-testid="inline-save-btn"]').click(); break; @@ -191,7 +207,9 @@ export const setValueForProperty = async (data: { await page.locator(`[data-testid="${val}"]`).click(); } - await page.locator('[data-testid="inline-save-btn"]').click(); + await clickOutside(page); + + await container.locator('[data-testid="inline-save-btn"]').click(); break; } @@ -208,23 +226,43 @@ export const validateValueForProperty = async (data: { const { page, propertyName, value, propertyType } = data; await page.click('[data-testid="custom_properties"]'); + const container = page.locator( + `[data-testid="custom-property-${propertyName}-card"]` + ); + + const toggleBtnVisibility = await container + .locator(`[data-testid="toggle-${propertyName}"]`) + .isVisible(); + + if (toggleBtnVisibility) { + await container.locator(`[data-testid="toggle-${propertyName}"]`).click(); + } + if (propertyType === 'enum') { - await expect( - page.getByLabel('Custom Properties').getByTestId('enum-value') - ).toContainText(value); + await expect(container.getByTestId('enum-value')).toContainText(value); } else if (propertyType === 'timeInterval') { const [startValue, endValue] = value.split(','); - await expect( - page.getByLabel('Custom Properties').getByTestId('time-interval-value') - ).toContainText(startValue); - await expect( - page.getByLabel('Custom Properties').getByTestId('time-interval-value') - ).toContainText(endValue); + await expect(container.getByTestId('time-interval-value')).toContainText( + startValue + ); + await expect(container.getByTestId('time-interval-value')).toContainText( + endValue + ); } else if (propertyType === 'sqlQuery') { + await expect(container.locator('.CodeMirror-scroll')).toContainText(value); + } else if (propertyType === 'enumWithDescriptions') { await expect( - page.getByLabel('Custom Properties').locator('.CodeMirror-scroll') - ).toContainText(value); + container.locator('[data-testid="enum-with-description-table"]') + ).toBeVisible(); + + await expect( + container + .locator('[data-testid="enum-with-description-table"]') + .getByText(value, { + exact: true, + }) + ).toBeVisible(); } else if ( ![ 'entityReference', @@ -233,9 +271,7 @@ export const validateValueForProperty = async (data: { 'dateTime-cp', ].includes(propertyType) ) { - await expect(page.getByRole('row', { name: propertyName })).toContainText( - value.replace(/\*|_/gi, '') - ); + await expect(container).toContainText(value.replace(/\*|_/gi, '')); } }; @@ -280,6 +316,11 @@ export const getPropertyValues = ( value: 'small', newValue: 'medium', }; + case 'enumWithDescriptions': + return { + value: 'enumWithDescription1', + newValue: 'enumWithDescription2', + }; case 'sqlQuery': return { value: 'Select * from table', @@ -403,6 +444,25 @@ export const createCustomPropertyForEntity = async ( }, } : {}), + ...(item.name === 'enumWithDescriptions' + ? { + customPropertyConfig: { + config: { + multiSelect: true, + values: [ + { + key: 'enumWithDescription1', + description: 'This is enumWithDescription1', + }, + { + key: 'enumWithDescription2', + description: 'This is enumWithDescription2', + }, + ], + }, + }, + } + : {}), ...(['entityReference', 'entityReferenceList'].includes(item.name) ? { customPropertyConfig: { @@ -465,12 +525,17 @@ export const addCustomPropertiesForEntity = async ({ enumConfig, formatConfig, entityReferenceConfig, + enumWithDescriptionConfig, }: { page: Page; propertyName: string; customPropertyData: { description: string }; customType: string; enumConfig?: { values: string[]; multiSelect: boolean }; + enumWithDescriptionConfig?: { + values: { key: string; description: string }[]; + multiSelect: boolean; + }; formatConfig?: string; entityReferenceConfig?: string[]; }) => { @@ -550,6 +615,27 @@ export const addCustomPropertiesForEntity = async ({ await page.click('#root\\/multiSelect'); } } + // Enum With Description configuration + if (customType === 'Enum With Descriptions' && enumWithDescriptionConfig) { + for await (const [ + index, + val, + ] of enumWithDescriptionConfig.values.entries()) { + await page.locator('[data-testid="add-enum-description-config"]').click(); + await page.locator(`#key-${index}`).fill(val.key); + await page.locator(descriptionBox).nth(index).fill(val.description); + } + await clickOutside(page); + + if (enumWithDescriptionConfig.multiSelect) { + await page.click('#root\\/multiSelect'); + } + + await page + .locator(descriptionBox) + .nth(2) + .fill(customPropertyData.description); + } // Entity reference configuration if ( @@ -577,7 +663,9 @@ export const addCustomPropertiesForEntity = async ({ } // Description - await page.fill(descriptionBox, customPropertyData.description); + if (customType !== 'Enum With Descriptions') { + await page.fill(descriptionBox, customPropertyData.description); + } const createPropertyPromise = page.waitForResponse( '/api/v1/metadata/types/name/*?fields=customProperties' @@ -612,10 +700,22 @@ export const editCreatedProperty = async ( ).toContainText('["enum1","enum2","enum3"]'); } + if (type === 'Enum With Descriptions') { + await expect( + page + .getByRole('row', { + name: `${propertyName} enumWithDescriptions enumWithDescription1`, + }) + .getByTestId('enum-with-description-config') + ).toBeVisible(); + } + await editButton.click(); - await page.locator(descriptionBox).fill(''); - await page.locator(descriptionBox).fill('This is new description'); + if (type !== 'Enum With Descriptions') { + await page.locator(descriptionBox).fill(''); + await page.locator(descriptionBox).fill('This is new description'); + } if (type === 'Enum') { await page.click('#root\\/customPropertyConfig'); @@ -623,6 +723,10 @@ export const editCreatedProperty = async ( await page.press('#root\\/customPropertyConfig', 'Enter'); await clickOutside(page); } + if (type === 'Enum With Descriptions') { + await page.locator(descriptionBox).nth(0).fill(''); + await page.locator(descriptionBox).nth(0).fill('This is new description'); + } if (ENTITY_REFERENCE_PROPERTIES.includes(type ?? '')) { await page.click('#root\\/customPropertyConfig'); @@ -655,6 +759,17 @@ export const editCreatedProperty = async ( ) ).toContainText('["enum1","enum2","enum3","updatedValue"]'); } + + if (type === 'Enum With Descriptions') { + await expect( + page + .getByRole('row', { + name: `${propertyName} enumWithDescriptions enumWithDescription1`, + }) + .getByTestId('enum-with-description-config') + ).toBeVisible(); + } + if (ENTITY_REFERENCE_PROPERTIES.includes(type ?? '')) { await expect( page.locator( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/CustomProperty/AddCustomProperty/AddCustomProperty.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/CustomProperty/AddCustomProperty/AddCustomProperty.tsx index 37e0773edf4..90eedab4e0f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/CustomProperty/AddCustomProperty/AddCustomProperty.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/CustomProperty/AddCustomProperty/AddCustomProperty.tsx @@ -11,7 +11,8 @@ * limitations under the License. */ -import { Button, Col, Form, Row } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import { Button, Col, Form, Input, Row } from 'antd'; import { AxiosError } from 'axios'; import { t } from 'i18next'; import { isUndefined, map, omit, omitBy, startCase } from 'lodash'; @@ -23,8 +24,10 @@ import React, { useState, } from 'react'; import { useHistory, useParams } from 'react-router-dom'; +import { ReactComponent as DeleteIcon } from '../../../../assets/svg/ic-delete.svg'; import { ENTITY_REFERENCE_OPTIONS, + ENUM_WITH_DESCRIPTION, PROPERTY_TYPES_WITH_ENTITY_REFERENCE, PROPERTY_TYPES_WITH_FORMAT, SUPPORTED_FORMAT_MAP, @@ -38,6 +41,7 @@ import { import { EntityType } from '../../../../enums/entity.enum'; import { ServiceCategory } from '../../../../enums/service.enum'; import { Category, Type } from '../../../../generated/entity/type'; +import { EnumWithDescriptionsConfig } from '../../../../generated/type/customProperties/enumWithDescriptionsConfig'; import { CustomProperty } from '../../../../generated/type/customProperty'; import { FieldProp, @@ -54,6 +58,7 @@ import { getSettingOptionByEntityType } from '../../../../utils/GlobalSettingsUt import { getSettingPath } from '../../../../utils/RouterUtils'; import { showErrorToast } from '../../../../utils/ToastUtils'; import ResizablePanels from '../../../common/ResizablePanels/ResizablePanels'; +import RichTextEditor from '../../../common/RichTextEditor/RichTextEditor'; import ServiceDocPanel from '../../../common/ServiceDocPanel/ServiceDocPanel'; import TitleBreadcrumb from '../../../common/TitleBreadcrumb/TitleBreadcrumb.component'; @@ -107,6 +112,7 @@ const AddCustomProperty = () => { hasFormatConfig, hasEntityReferenceConfig, watchedOption, + hasEnumWithDescriptionConfig, } = useMemo(() => { const watchedOption = propertyTypeOptions.find( (option) => option.value === watchedPropertyType @@ -115,6 +121,9 @@ const AddCustomProperty = () => { const hasEnumConfig = watchedOptionKey === 'enum'; + const hasEnumWithDescriptionConfig = + watchedOptionKey === ENUM_WITH_DESCRIPTION; + const hasFormatConfig = PROPERTY_TYPES_WITH_FORMAT.includes(watchedOptionKey); @@ -126,6 +135,7 @@ const AddCustomProperty = () => { hasFormatConfig, hasEntityReferenceConfig, watchedOption, + hasEnumWithDescriptionConfig, }; }, [watchedPropertyType, propertyTypeOptions]); @@ -166,6 +176,7 @@ const AddCustomProperty = () => { formatConfig: string; entityReferenceConfig: string[]; multiSelect?: boolean; + enumWithDescriptionsConfig?: EnumWithDescriptionsConfig['values']; } ) => { if (isUndefined(typeDetail)) { @@ -197,6 +208,15 @@ const AddCustomProperty = () => { }; } + if (hasEnumWithDescriptionConfig) { + customPropertyConfig = { + config: { + multiSelect: Boolean(data?.multiSelect), + values: data.enumWithDescriptionsConfig, + }, + }; + } + const payload = omitBy( { ...omit(data, [ @@ -204,6 +224,7 @@ const AddCustomProperty = () => { 'formatConfig', 'entityReferenceConfig', 'enumConfig', + 'enumWithDescriptionsConfig', ]), propertyType: { id: data.propertyType, @@ -393,6 +414,95 @@ const AddCustomProperty = () => { hasEntityReferenceConfig && generateFormFields([entityReferenceConfigField]) } + + {hasEnumWithDescriptionConfig && ( + <> + + {(fields, { add, remove }) => ( + <> + +