mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-13 17:32:53 +00:00
Add Custom Propety Config to store format, enum values, entity types (#15302)
* Add Custom Propety Config to store format, enum values, entity types * Fix import statements and remove unused code * Add Custom Propety Config to store format, enum values, entity types * Add support for enum field type in custom properties * update name in customPropertyConfigTypeValueField * add custom property config column in custom property table * Update padding-left in block-editor.less * Add enum value translation for multiple languages * update placeholder of config * fixed python sdk * add enum type in property value * add unit tests * Add Custom Propety Config to store format, enum values, entity types * update ui to handle the enum config and validation * Fix enum value handling in EditCustomPropertyModal and PropertyValue * Update CustomProperty.md with enum values and multi-select option * add cypress test * add cypress for multiselect enum value * Add tests for enum props * add cypress for editing the enum property * Add validations to enum * Fix dependency issue --------- Co-authored-by: Sachin Chaurasiya <sachinchaurasiyachotey87@gmail.com> Co-authored-by: Onkar Ravgan <onkar.10r@gmail.com>
This commit is contained in:
parent
e414e9eaa3
commit
cecbf80a2d
@ -41,7 +41,6 @@ class CustomPropertyDataTypes(Enum):
|
||||
|
||||
class OMetaCustomProperties(BaseModel):
|
||||
entity_type: Type[T]
|
||||
custom_property_type: Optional[CustomPropertyDataTypes]
|
||||
createCustomPropertyRequest: CreateCustomPropertyRequest
|
||||
|
||||
|
||||
|
||||
@ -15,7 +15,8 @@ To be used by OpenMetadata class
|
||||
"""
|
||||
from typing import Dict
|
||||
|
||||
from metadata.generated.schema.api.data.createCustomProperty import PropertyType
|
||||
from metadata.generated.schema.type.customProperty import PropertyType
|
||||
from metadata.generated.schema.type.entityReference import EntityReference
|
||||
from metadata.ingestion.models.custom_properties import (
|
||||
CustomPropertyDataTypes,
|
||||
CustomPropertyType,
|
||||
@ -54,16 +55,6 @@ class OMetaCustomPropertyMixin:
|
||||
f"/metadata/types/name/{entity_type}?category=field"
|
||||
)
|
||||
|
||||
# Get the data type of the custom property
|
||||
if not ometa_custom_property.createCustomPropertyRequest.propertyType:
|
||||
custom_property_type = self.get_custom_property_type(
|
||||
data_type=ometa_custom_property.custom_property_type
|
||||
)
|
||||
property_type = PropertyType(id=custom_property_type.id, type="type")
|
||||
ometa_custom_property.createCustomPropertyRequest.propertyType = (
|
||||
property_type
|
||||
)
|
||||
|
||||
resp = self.client.put(
|
||||
f"/metadata/types/{entity_schema.get('id')}",
|
||||
data=ometa_custom_property.createCustomPropertyRequest.json(),
|
||||
@ -78,3 +69,12 @@ class OMetaCustomPropertyMixin:
|
||||
"""
|
||||
resp = self.client.get(f"/metadata/types/name/{data_type.value}?category=field")
|
||||
return CustomPropertyType(**resp)
|
||||
|
||||
def get_property_type_ref(self, data_type: CustomPropertyDataTypes) -> PropertyType:
|
||||
"""
|
||||
Get the PropertyType for custom properties
|
||||
"""
|
||||
custom_property_type = self.get_custom_property_type(data_type=data_type)
|
||||
return PropertyType(
|
||||
__root__=EntityReference(id=custom_property_type.id, type="type")
|
||||
)
|
||||
|
||||
@ -144,9 +144,12 @@ class OMetaCustomAttributeTest(TestCase):
|
||||
# Create the table size property
|
||||
ometa_custom_property_request = OMetaCustomProperties(
|
||||
entity_type=Table,
|
||||
custom_property_type=CustomPropertyDataTypes.STRING,
|
||||
createCustomPropertyRequest=CreateCustomPropertyRequest(
|
||||
name="TableSize", description="Size of the Table"
|
||||
name="TableSize",
|
||||
description="Size of the Table",
|
||||
propertyType=self.metadata.get_property_type_ref(
|
||||
CustomPropertyDataTypes.STRING
|
||||
),
|
||||
),
|
||||
)
|
||||
self.metadata.create_or_update_custom_property(
|
||||
@ -156,9 +159,12 @@ class OMetaCustomAttributeTest(TestCase):
|
||||
# Create the DataQuality property for a table
|
||||
ometa_custom_property_request = OMetaCustomProperties(
|
||||
entity_type=Table,
|
||||
custom_property_type=CustomPropertyDataTypes.MARKDOWN,
|
||||
createCustomPropertyRequest=CreateCustomPropertyRequest(
|
||||
name="DataQuality", description="Quality Details of a Table"
|
||||
name="DataQuality",
|
||||
description="Quality Details of a Table",
|
||||
propertyType=self.metadata.get_property_type_ref(
|
||||
CustomPropertyDataTypes.MARKDOWN
|
||||
),
|
||||
),
|
||||
)
|
||||
self.metadata.create_or_update_custom_property(
|
||||
@ -168,9 +174,12 @@ class OMetaCustomAttributeTest(TestCase):
|
||||
# Create the SchemaCost property for database schema
|
||||
ometa_custom_property_request = OMetaCustomProperties(
|
||||
entity_type=DatabaseSchema,
|
||||
custom_property_type=CustomPropertyDataTypes.INTEGER,
|
||||
createCustomPropertyRequest=CreateCustomPropertyRequest(
|
||||
name="SchemaAge", description="Age in years of a Schema"
|
||||
name="SchemaAge",
|
||||
description="Age in years of a Schema",
|
||||
propertyType=self.metadata.get_property_type_ref(
|
||||
CustomPropertyDataTypes.INTEGER
|
||||
),
|
||||
),
|
||||
)
|
||||
self.metadata.create_or_update_custom_property(
|
||||
|
||||
@ -23,6 +23,7 @@ import static org.openmetadata.service.util.EntityUtil.customFieldMatch;
|
||||
import static org.openmetadata.service.util.EntityUtil.getCustomField;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
@ -32,9 +33,11 @@ import org.jdbi.v3.sqlobject.transaction.Transaction;
|
||||
import org.openmetadata.schema.entity.Type;
|
||||
import org.openmetadata.schema.entity.type.Category;
|
||||
import org.openmetadata.schema.entity.type.CustomProperty;
|
||||
import org.openmetadata.schema.type.CustomPropertyConfig;
|
||||
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.service.Entity;
|
||||
import org.openmetadata.service.TypeRegistry;
|
||||
import org.openmetadata.service.resources.types.TypeResource;
|
||||
@ -117,6 +120,7 @@ public class TypeRepository extends EntityRepository<Type> {
|
||||
property.setPropertyType(
|
||||
Entity.getEntityReferenceById(
|
||||
Entity.TYPE, property.getPropertyType().getId(), NON_DELETED));
|
||||
validateProperty(property);
|
||||
if (type.getCategory().equals(Category.Field)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Only entity types can be extended and field types can't be extended");
|
||||
@ -161,6 +165,30 @@ public class TypeRepository extends EntityRepository<Type> {
|
||||
return customProperties;
|
||||
}
|
||||
|
||||
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 "int", "string" -> {}
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles entity updated from PUT and POST operation. */
|
||||
public class TypeUpdater extends EntityUpdater {
|
||||
public TypeUpdater(Type original, Type updated, Operation operation) {
|
||||
@ -199,6 +227,7 @@ public class TypeRepository extends EntityRepository<Type> {
|
||||
continue;
|
||||
}
|
||||
updateCustomPropertyDescription(updated, storedProperty, updateProperty);
|
||||
updateCustomPropertyConfig(updated, storedProperty, updateProperty);
|
||||
}
|
||||
}
|
||||
|
||||
@ -270,5 +299,55 @@ public class TypeRepository extends EntityRepository<Type> {
|
||||
customPropertyJson);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCustomPropertyConfig(
|
||||
Type entity, CustomProperty origProperty, CustomProperty updatedProperty) {
|
||||
String fieldName = getCustomField(origProperty, "customPropertyConfig");
|
||||
if (previous == null || !previous.getVersion().equals(updated.getVersion())) {
|
||||
validatePropertyConfigUpdate(entity, origProperty, updatedProperty);
|
||||
}
|
||||
if (recordChange(
|
||||
fieldName,
|
||||
origProperty.getCustomPropertyConfig(),
|
||||
updatedProperty.getCustomPropertyConfig())) {
|
||||
String customPropertyFQN =
|
||||
getCustomPropertyFQN(entity.getName(), updatedProperty.getName());
|
||||
EntityReference propertyType =
|
||||
updatedProperty.getPropertyType(); // Don't store entity reference
|
||||
String customPropertyJson = JsonUtils.pojoToJson(updatedProperty.withPropertyType(null));
|
||||
updatedProperty.withPropertyType(propertyType); // Restore entity reference
|
||||
daoCollection
|
||||
.fieldRelationshipDAO()
|
||||
.upsert(
|
||||
customPropertyFQN,
|
||||
updatedProperty.getPropertyType().getName(),
|
||||
customPropertyFQN,
|
||||
updatedProperty.getPropertyType().getName(),
|
||||
Entity.TYPE,
|
||||
Entity.TYPE,
|
||||
Relationship.HAS.ordinal(),
|
||||
"customProperty",
|
||||
customPropertyJson);
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePropertyConfigUpdate(
|
||||
Type entity, CustomProperty origProperty, CustomProperty updatedProperty) {
|
||||
if (origProperty.getPropertyType().getName().equals("enum")) {
|
||||
EnumConfig origConfig =
|
||||
JsonUtils.convertValue(
|
||||
origProperty.getCustomPropertyConfig().getConfig(), EnumConfig.class);
|
||||
EnumConfig updatedConfig =
|
||||
JsonUtils.convertValue(
|
||||
updatedProperty.getCustomPropertyConfig().getConfig(), EnumConfig.class);
|
||||
HashSet<String> updatedValues = new HashSet<>(updatedConfig.getValues());
|
||||
if (updatedValues.size() != updatedConfig.getValues().size()) {
|
||||
throw new IllegalArgumentException("Enum Custom Property values cannot have duplicates.");
|
||||
} else if (!updatedValues.containsAll(origConfig.getValues())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Existing Enum Custom Property values cannot be removed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,6 +100,7 @@ import org.apache.http.client.HttpResponseException;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
@ -364,6 +365,8 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
|
||||
public static Type INT_TYPE;
|
||||
public static Type STRING_TYPE;
|
||||
|
||||
public static Type ENUM_TYPE;
|
||||
|
||||
// Run webhook related tests randomly. This will ensure these tests are not run for every entity
|
||||
// evey time junit tests are run to save time. But over the course of development of a release,
|
||||
// when tests are run enough times, the webhook tests are run for all the entities.
|
||||
@ -1877,7 +1880,7 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
|
||||
}
|
||||
|
||||
@Test
|
||||
protected void checkIndexCreated() throws IOException {
|
||||
protected void checkIndexCreated() throws IOException, JSONException {
|
||||
if (RUN_ELASTIC_SEARCH_TESTCASES) {
|
||||
RestClient client = getSearchClient();
|
||||
Request request = new Request("GET", "/_cat/indices");
|
||||
|
||||
@ -42,7 +42,9 @@ import org.openmetadata.schema.entity.Type;
|
||||
import org.openmetadata.schema.entity.type.Category;
|
||||
import org.openmetadata.schema.entity.type.CustomProperty;
|
||||
import org.openmetadata.schema.type.ChangeDescription;
|
||||
import org.openmetadata.schema.type.CustomPropertyConfig;
|
||||
import org.openmetadata.schema.type.EntityReference;
|
||||
import org.openmetadata.schema.type.customproperties.EnumConfig;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.resources.EntityResourceTest;
|
||||
import org.openmetadata.service.resources.types.TypeResource;
|
||||
@ -66,6 +68,7 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
|
||||
public void setupTypes() throws HttpResponseException {
|
||||
INT_TYPE = getEntityByName("integer", "", ADMIN_AUTH_HEADERS);
|
||||
STRING_TYPE = getEntityByName("string", "", ADMIN_AUTH_HEADERS);
|
||||
ENUM_TYPE = getEntityByName("enum", "", ADMIN_AUTH_HEADERS);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -86,8 +89,8 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
|
||||
|
||||
@Test
|
||||
void put_patch_customProperty_200() throws IOException {
|
||||
Type tableEntity = getEntityByName("table", "customProperties", ADMIN_AUTH_HEADERS);
|
||||
assertTrue(listOrEmpty(tableEntity.getCustomProperties()).isEmpty());
|
||||
Type topicEntity = getEntityByName("topic", "customProperties", ADMIN_AUTH_HEADERS);
|
||||
assertTrue(listOrEmpty(topicEntity.getCustomProperties()).isEmpty());
|
||||
|
||||
// Add a custom property with name intA with type integer with PUT
|
||||
CustomProperty fieldA =
|
||||
@ -95,34 +98,170 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
|
||||
.withName("intA")
|
||||
.withDescription("intA")
|
||||
.withPropertyType(INT_TYPE.getEntityReference());
|
||||
ChangeDescription change = getChangeDescription(tableEntity, MINOR_UPDATE);
|
||||
ChangeDescription change = getChangeDescription(topicEntity, MINOR_UPDATE);
|
||||
fieldAdded(change, "customProperties", new ArrayList<>(List.of(fieldA)));
|
||||
tableEntity =
|
||||
topicEntity =
|
||||
addCustomPropertyAndCheck(
|
||||
tableEntity.getId(), fieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
|
||||
assertCustomProperties(new ArrayList<>(List.of(fieldA)), tableEntity.getCustomProperties());
|
||||
topicEntity.getId(), fieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
|
||||
assertCustomProperties(new ArrayList<>(List.of(fieldA)), topicEntity.getCustomProperties());
|
||||
|
||||
// Changing custom property description with PUT
|
||||
fieldA.withDescription("updated");
|
||||
change = getChangeDescription(tableEntity, MINOR_UPDATE);
|
||||
change = getChangeDescription(topicEntity, MINOR_UPDATE);
|
||||
fieldUpdated(change, EntityUtil.getCustomField(fieldA, "description"), "intA", "updated");
|
||||
tableEntity =
|
||||
topicEntity =
|
||||
addCustomPropertyAndCheck(
|
||||
tableEntity.getId(), fieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
|
||||
assertCustomProperties(new ArrayList<>(List.of(fieldA)), tableEntity.getCustomProperties());
|
||||
topicEntity.getId(), fieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
|
||||
assertCustomProperties(new ArrayList<>(List.of(fieldA)), topicEntity.getCustomProperties());
|
||||
|
||||
// Changing custom property description with PATCH
|
||||
// Changes from this PATCH is consolidated with the previous changes
|
||||
fieldA.withDescription("updated2");
|
||||
String json = JsonUtils.pojoToJson(tableEntity);
|
||||
tableEntity.setCustomProperties(List.of(fieldA));
|
||||
change = getChangeDescription(tableEntity, CHANGE_CONSOLIDATED);
|
||||
String json = JsonUtils.pojoToJson(topicEntity);
|
||||
topicEntity.setCustomProperties(List.of(fieldA));
|
||||
change = getChangeDescription(topicEntity, CHANGE_CONSOLIDATED);
|
||||
fieldUpdated(change, EntityUtil.getCustomField(fieldA, "description"), "intA", "updated2");
|
||||
tableEntity =
|
||||
patchEntityAndCheck(tableEntity, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change);
|
||||
topicEntity =
|
||||
patchEntityAndCheck(topicEntity, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change);
|
||||
|
||||
// Add a second property with name intB with type integer
|
||||
// Note that since this is PUT operation, the previous changes are not consolidated
|
||||
EntityReference typeRef =
|
||||
new EntityReference()
|
||||
.withType(INT_TYPE.getEntityReference().getType())
|
||||
.withId(INT_TYPE.getEntityReference().getId());
|
||||
CustomProperty fieldB =
|
||||
new CustomProperty().withName("intB").withDescription("intB").withPropertyType(typeRef);
|
||||
change = getChangeDescription(topicEntity, MINOR_UPDATE);
|
||||
fieldAdded(change, "customProperties", new ArrayList<>(List.of(fieldB)));
|
||||
topicEntity =
|
||||
addCustomPropertyAndCheck(
|
||||
topicEntity.getId(), fieldB, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
|
||||
fieldB.setPropertyType(INT_TYPE.getEntityReference());
|
||||
assertEquals(2, topicEntity.getCustomProperties().size());
|
||||
assertCustomProperties(
|
||||
new ArrayList<>(List.of(fieldA, fieldB)), topicEntity.getCustomProperties());
|
||||
}
|
||||
|
||||
@Test
|
||||
void put_patch_customProperty_enum_200() throws IOException {
|
||||
Type tableEntity = getEntityByName("table", "customProperties", ADMIN_AUTH_HEADERS);
|
||||
assertTrue(listOrEmpty(tableEntity.getCustomProperties()).isEmpty());
|
||||
|
||||
// Add a custom property with name intA with type integer with PUT
|
||||
CustomProperty enumFieldA =
|
||||
new CustomProperty()
|
||||
.withName("enumTest")
|
||||
.withDescription("enumTest")
|
||||
.withPropertyType(ENUM_TYPE.getEntityReference());
|
||||
ChangeDescription change = getChangeDescription(tableEntity, MINOR_UPDATE);
|
||||
fieldAdded(change, "customProperties", new ArrayList<>(List.of(enumFieldA)));
|
||||
Type finalTableEntity = tableEntity;
|
||||
ChangeDescription finalChange = change;
|
||||
assertResponseContains(
|
||||
() ->
|
||||
addCustomPropertyAndCheck(
|
||||
finalTableEntity.getId(),
|
||||
enumFieldA,
|
||||
ADMIN_AUTH_HEADERS,
|
||||
MINOR_UPDATE,
|
||||
finalChange),
|
||||
Status.BAD_REQUEST,
|
||||
"Enum Custom Property Type must have EnumConfig.");
|
||||
enumFieldA.setCustomPropertyConfig(new CustomPropertyConfig().withConfig(new EnumConfig()));
|
||||
ChangeDescription change1 = getChangeDescription(tableEntity, MINOR_UPDATE);
|
||||
Type tableEntity1 = tableEntity;
|
||||
assertResponseContains(
|
||||
() ->
|
||||
addCustomPropertyAndCheck(
|
||||
tableEntity1.getId(), enumFieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change1),
|
||||
Status.BAD_REQUEST,
|
||||
"Enum Custom Property Type must have EnumConfig populated with values.");
|
||||
|
||||
enumFieldA.setCustomPropertyConfig(
|
||||
new CustomPropertyConfig()
|
||||
.withConfig(new EnumConfig().withValues(List.of("A", "B", "C", "C"))));
|
||||
ChangeDescription change7 = getChangeDescription(tableEntity, MINOR_UPDATE);
|
||||
Type tableEntity2 = tableEntity;
|
||||
assertResponseContains(
|
||||
() ->
|
||||
addCustomPropertyAndCheck(
|
||||
tableEntity2.getId(), enumFieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change7),
|
||||
Status.BAD_REQUEST,
|
||||
"Enum Custom Property values cannot have duplicates.");
|
||||
|
||||
enumFieldA.setCustomPropertyConfig(
|
||||
new CustomPropertyConfig().withConfig(new EnumConfig().withValues(List.of("A", "B", "C"))));
|
||||
tableEntity =
|
||||
addCustomPropertyAndCheck(
|
||||
tableEntity.getId(), enumFieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
|
||||
assertCustomProperties(new ArrayList<>(List.of(enumFieldA)), tableEntity.getCustomProperties());
|
||||
CustomPropertyConfig prevConfig = enumFieldA.getCustomPropertyConfig();
|
||||
// Changing custom property description with PUT
|
||||
enumFieldA.withDescription("updatedEnumTest");
|
||||
ChangeDescription change2 = getChangeDescription(tableEntity, MINOR_UPDATE);
|
||||
fieldUpdated(
|
||||
change2,
|
||||
EntityUtil.getCustomField(enumFieldA, "description"),
|
||||
"enumTest",
|
||||
"updatedEnumTest");
|
||||
tableEntity =
|
||||
addCustomPropertyAndCheck(
|
||||
tableEntity.getId(), enumFieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change2);
|
||||
assertCustomProperties(new ArrayList<>(List.of(enumFieldA)), tableEntity.getCustomProperties());
|
||||
|
||||
enumFieldA.setCustomPropertyConfig(
|
||||
new CustomPropertyConfig().withConfig(new EnumConfig().withValues(List.of("A", "B"))));
|
||||
ChangeDescription change3 = getChangeDescription(tableEntity, MINOR_UPDATE);
|
||||
assertResponseContains(
|
||||
() ->
|
||||
addCustomPropertyAndCheck(
|
||||
tableEntity1.getId(), enumFieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change3),
|
||||
Status.BAD_REQUEST,
|
||||
"Existing Enum Custom Property values cannot be removed.");
|
||||
|
||||
enumFieldA.setCustomPropertyConfig(
|
||||
new CustomPropertyConfig()
|
||||
.withConfig(new EnumConfig().withValues(List.of("A", "B", "C", "C"))));
|
||||
ChangeDescription change4 = getChangeDescription(tableEntity, MINOR_UPDATE);
|
||||
assertResponseContains(
|
||||
() ->
|
||||
addCustomPropertyAndCheck(
|
||||
tableEntity1.getId(), enumFieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change4),
|
||||
Status.BAD_REQUEST,
|
||||
"Enum Custom Property values cannot have duplicates.");
|
||||
|
||||
ChangeDescription change5 = getChangeDescription(tableEntity, MINOR_UPDATE);
|
||||
enumFieldA.setCustomPropertyConfig(
|
||||
new CustomPropertyConfig()
|
||||
.withConfig(new EnumConfig().withValues(List.of("A", "B", "C", "D"))));
|
||||
fieldUpdated(
|
||||
change5,
|
||||
EntityUtil.getCustomField(enumFieldA, "customPropertyConfig"),
|
||||
prevConfig,
|
||||
enumFieldA.getCustomPropertyConfig());
|
||||
tableEntity =
|
||||
addCustomPropertyAndCheck(
|
||||
tableEntity.getId(), enumFieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change5);
|
||||
assertCustomProperties(new ArrayList<>(List.of(enumFieldA)), tableEntity.getCustomProperties());
|
||||
|
||||
// Changing custom property description with PATCH
|
||||
// Changes from this PATCH is consolidated with the previous changes
|
||||
enumFieldA.withDescription("updated2");
|
||||
String json = JsonUtils.pojoToJson(tableEntity);
|
||||
tableEntity.setCustomProperties(List.of(enumFieldA));
|
||||
change = getChangeDescription(tableEntity, CHANGE_CONSOLIDATED);
|
||||
fieldUpdated(
|
||||
change5,
|
||||
EntityUtil.getCustomField(enumFieldA, "description"),
|
||||
"updatedEnumTest",
|
||||
"updated2");
|
||||
|
||||
tableEntity =
|
||||
patchEntityAndCheck(tableEntity, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change5);
|
||||
|
||||
/* // Add a second property with name intB with type integer
|
||||
// Note that since this is PUT operation, the previous changes are not consolidated
|
||||
EntityReference typeRef =
|
||||
new EntityReference()
|
||||
.withType(INT_TYPE.getEntityReference().getType())
|
||||
@ -137,7 +276,7 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
|
||||
fieldB.setPropertyType(INT_TYPE.getEntityReference());
|
||||
assertEquals(2, tableEntity.getCustomProperties().size());
|
||||
assertCustomProperties(
|
||||
new ArrayList<>(List.of(fieldA, fieldB)), tableEntity.getCustomProperties());
|
||||
new ArrayList<>(List.of(fieldA, fieldB)), tableEntity.getCustomProperties());*/
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -225,7 +364,6 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
|
||||
assertEquals(expected.getSchema(), patched.getSchema());
|
||||
assertEquals(expected.getCategory(), patched.getCategory());
|
||||
assertEquals(expected.getNameSpace(), patched.getNameSpace());
|
||||
assertEquals(expected.getCustomProperties(), patched.getCustomProperties());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -239,6 +377,9 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
|
||||
List<CustomProperty> actualProperties =
|
||||
JsonUtils.readObjects(actual.toString(), CustomProperty.class);
|
||||
TestUtils.assertCustomProperties(expectedProperties, actualProperties);
|
||||
} else if (fieldName.contains("customPropertyConfig")) {
|
||||
String expectedStr = JsonUtils.pojoToJson(expected);
|
||||
String actualStr = JsonUtils.pojoToJson(actual);
|
||||
} else {
|
||||
assertCommonFieldChange(fieldName, expected, actual);
|
||||
}
|
||||
|
||||
@ -4,23 +4,6 @@
|
||||
"title": "CreateCustomPropertyRequest",
|
||||
"description": "Create Custom Property Model entity request",
|
||||
"type": "object",
|
||||
"definitions": {
|
||||
"propertyType": {
|
||||
"description": "Property Type",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"description": "Property type",
|
||||
"type": "string",
|
||||
"default": "type"
|
||||
},
|
||||
"id": {
|
||||
"description": "Unique identifier of this instance.",
|
||||
"$ref": "../../type/basic.json#/definitions/uuid"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name that identifies this Custom Property model.",
|
||||
@ -31,10 +14,14 @@
|
||||
"$ref": "../../type/basic.json#/definitions/markdown"
|
||||
},
|
||||
"propertyType": {
|
||||
"description": "Property Type",
|
||||
"$ref": "#/definitions/propertyType"
|
||||
"description": "Property Type.",
|
||||
"$ref": "../../type/customProperty.json#/definitions/propertyType"
|
||||
},
|
||||
"customPropertyConfig": {
|
||||
"description": "Config to define constraints around CustomProperty.",
|
||||
"$ref": "../../type/customProperty.json#/definitions/customPropertyConfig"
|
||||
}
|
||||
},
|
||||
"required": ["name"],
|
||||
"required": ["name", "propertyType"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
||||
@ -25,26 +25,6 @@
|
||||
"name": "Entity"
|
||||
}
|
||||
]
|
||||
},
|
||||
"customProperty": {
|
||||
"description": "Type used for adding custom property to an entity to extend it.",
|
||||
"type": "object",
|
||||
"javaType": "org.openmetadata.schema.entity.type.CustomProperty",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name of the entity property. Note a property name must be unique for an entity. Property name must follow camelCase naming adopted by openMetadata - must start with lower case with no space, underscore, or dots.",
|
||||
"$ref": "#/definitions/entityName"
|
||||
},
|
||||
"description": {
|
||||
"$ref": "../type/basic.json#/definitions/markdown"
|
||||
},
|
||||
"propertyType": {
|
||||
"description": "Reference to a property type. Only property types are allowed and entity types are not allowed as custom properties to extend an existing entity",
|
||||
"$ref": "../type/entityReference.json"
|
||||
}
|
||||
},
|
||||
"required": ["name", "description", "propertyType"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
@ -84,7 +64,7 @@
|
||||
"description": "Custom properties added to extend the entity. Only available for entity type",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/customProperty"
|
||||
"$ref": "../type/customProperty.json"
|
||||
}
|
||||
},
|
||||
"version": {
|
||||
|
||||
@ -85,6 +85,14 @@
|
||||
"type": "string",
|
||||
"format": "time"
|
||||
},
|
||||
"enum": {
|
||||
"$comment" : "@om-field-type",
|
||||
"description": "List of values in Enum.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"timezone": {
|
||||
"description": "Timezone of the user in the format `America/Los_Angeles`, `Brazil/East`, etc.",
|
||||
"type": "string",
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
{
|
||||
"$id": "https://open-metadata.org/schema/type/customPropertyEnumConfig.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "EnumConfig",
|
||||
"type": "object",
|
||||
"javaType": "org.openmetadata.schema.type.customproperties.EnumConfig",
|
||||
"description": "Applies to Enum type, this config is used to define list of enum values",
|
||||
"properties": {
|
||||
"multiSelect": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"values": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
{
|
||||
"$id": "https://open-metadata.org/schema/type/customProperty.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "CustomProperty",
|
||||
"description": "This schema defines the custom property to an entity to extend it.",
|
||||
"type": "object",
|
||||
"javaType": "org.openmetadata.schema.entity.type.CustomProperty",
|
||||
"definitions": {
|
||||
"format": {
|
||||
"description": "Applies to date interval, date, time format.",
|
||||
"type": "string"
|
||||
},
|
||||
"entityTypes": {
|
||||
"description": "Applies to Entity References. Entity Types can be used to restrict what type of entities can be configured for a entity reference.",
|
||||
"type": "string"
|
||||
},
|
||||
"customPropertyConfig": {
|
||||
"type": "object",
|
||||
"javaType": "org.openmetadata.schema.type.CustomPropertyConfig",
|
||||
"title": "CustomPropertyConfig",
|
||||
"description": "Config to define constraints around CustomProperty",
|
||||
"properties": {
|
||||
"config": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "../type/customProperties/enumConfig.json"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/format"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/entityTypes"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"propertyType": {
|
||||
"description": "Reference to a property type. Only property types are allowed and entity types are not allowed as custom properties to extend an existing entity",
|
||||
"$ref": "../type/entityReference.json"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name of the entity property. Note a property name must be unique for an entity. Property name must follow camelCase naming adopted by openMetadata - must start with lower case with no space, underscore, or dots.",
|
||||
"$ref": "../type/basic.json#/definitions/entityName"
|
||||
},
|
||||
"description": {
|
||||
"$ref": "../type/basic.json#/definitions/markdown"
|
||||
},
|
||||
"propertyType": {
|
||||
"$ref": "#/definitions/propertyType"
|
||||
},
|
||||
"customPropertyConfig": {
|
||||
"$ref": "#/definitions/customPropertyConfig"
|
||||
}
|
||||
},
|
||||
"required": ["name", "description", "propertyType"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@ -596,6 +596,18 @@ export const addCustomPropertiesForEntity = (
|
||||
cy.get('[data-testid="propertyType"]').click();
|
||||
cy.get(`[title="${customType}"]`).click();
|
||||
|
||||
if (customType === 'Enum') {
|
||||
value.values.forEach((val) => {
|
||||
cy.get('#root\\/customPropertyConfig').type(`${val}{enter}`);
|
||||
});
|
||||
|
||||
cy.clickOutside();
|
||||
|
||||
if (value.multiSelect) {
|
||||
cy.get('#root\\/multiSelect').scrollIntoView().click();
|
||||
}
|
||||
}
|
||||
|
||||
cy.get(descriptionBox).clear().type(customPropertyData.description);
|
||||
|
||||
// Check if the property got added
|
||||
@ -611,19 +623,31 @@ export const addCustomPropertiesForEntity = (
|
||||
cy.clickOnLogo();
|
||||
};
|
||||
|
||||
export const editCreatedProperty = (propertyName) => {
|
||||
export const editCreatedProperty = (propertyName, type) => {
|
||||
// Fetching for edit button
|
||||
cy.get(`[data-row-key="${propertyName}"]`)
|
||||
.find('[data-testid="edit-button"]')
|
||||
.as('editButton');
|
||||
|
||||
if (type === 'Enum') {
|
||||
cy.get(`[data-row-key="${propertyName}"]`)
|
||||
.find('[data-testid="enum-config"]')
|
||||
.should('contain', '["enum1","enum2","enum3"]');
|
||||
}
|
||||
|
||||
cy.get('@editButton').click();
|
||||
|
||||
cy.get(descriptionBox).clear().type('This is new description');
|
||||
|
||||
if (type === 'Enum') {
|
||||
cy.get('#root\\/customPropertyConfig').type(`updatedValue{enter}`);
|
||||
|
||||
cy.clickOutside();
|
||||
}
|
||||
|
||||
interceptURL('PATCH', '/api/v1/metadata/types/*', 'checkPatchForDescription');
|
||||
|
||||
cy.get('[data-testid="save"]').click();
|
||||
cy.get('button[type="submit"]').scrollIntoView().click();
|
||||
|
||||
cy.wait('@checkPatchForDescription', { timeout: 15000 });
|
||||
|
||||
@ -633,6 +657,12 @@ export const editCreatedProperty = (propertyName) => {
|
||||
cy.get(`[data-row-key="${propertyName}"]`)
|
||||
.find('[data-testid="viewer-container"]')
|
||||
.should('contain', 'This is new description');
|
||||
|
||||
if (type === 'Enum') {
|
||||
cy.get(`[data-row-key="${propertyName}"]`)
|
||||
.find('[data-testid="enum-config"]')
|
||||
.should('contain', '["enum1","enum2","enum3","updatedValue"]');
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteCreatedProperty = (propertyName) => {
|
||||
|
||||
@ -495,6 +495,10 @@ export const ENTITIES = {
|
||||
integerValue: '45',
|
||||
stringValue: 'This is string propery',
|
||||
markdownValue: 'This is markdown value',
|
||||
enumConfig: {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
entityObj: SEARCH_ENTITY_TABLE.table_1,
|
||||
entityApiType: 'tables',
|
||||
},
|
||||
@ -504,6 +508,10 @@ export const ENTITIES = {
|
||||
integerValue: '23',
|
||||
stringValue: 'This is string propery',
|
||||
markdownValue: 'This is markdown value',
|
||||
enumConfig: {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
entityObj: SEARCH_ENTITY_TOPIC.topic_1,
|
||||
entityApiType: 'topics',
|
||||
},
|
||||
@ -523,6 +531,10 @@ export const ENTITIES = {
|
||||
integerValue: '78',
|
||||
stringValue: 'This is string propery',
|
||||
markdownValue: 'This is markdown value',
|
||||
enumConfig: {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: true,
|
||||
},
|
||||
entityObj: SEARCH_ENTITY_PIPELINE.pipeline_1,
|
||||
entityApiType: 'pipelines',
|
||||
},
|
||||
|
||||
@ -459,6 +459,66 @@ describe('Custom Properties should work properly', { tags: 'Settings' }, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Add update and delete Enum custom properties', () => {
|
||||
Object.values(ENTITIES).forEach((entity) => {
|
||||
const propertyName = `addcyentity${entity.name}test${uuid()}`;
|
||||
|
||||
it(`Add Enum custom property for ${entity.name} Entities`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
|
||||
addCustomPropertiesForEntity(
|
||||
propertyName,
|
||||
entity,
|
||||
'Enum',
|
||||
entity.enumConfig,
|
||||
entity.entityObj
|
||||
);
|
||||
|
||||
// Navigating back to custom properties page
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
});
|
||||
|
||||
it(`Edit created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
editCreatedProperty(propertyName, 'Enum');
|
||||
});
|
||||
|
||||
it(`Delete created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
deleteCreatedProperty(propertyName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom properties for glossary and glossary terms', () => {
|
||||
const propertyName = `addcyentity${glossaryTerm.name}test${uuid()}`;
|
||||
const properties = Object.values(CustomPropertyType).join(', ');
|
||||
|
||||
@ -18,4 +18,16 @@ $$section
|
||||
### Description $(id="description")
|
||||
|
||||
Describe your custom property to provide more information to your team.
|
||||
$$
|
||||
|
||||
$$section
|
||||
### Enum Values $(id="customPropertyConfig")
|
||||
|
||||
Add the list of values for enum property.
|
||||
$$
|
||||
|
||||
$$section
|
||||
### Multi Select $(id="multiSelect")
|
||||
|
||||
Enable multi select of values for enum property.
|
||||
$$
|
||||
@ -323,11 +323,11 @@
|
||||
|
||||
.om-list-decimal {
|
||||
list-style-type: decimal !important;
|
||||
padding-left: 16px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
.om-list-disc {
|
||||
list-style-type: disc !important;
|
||||
padding-left: 16px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
.om-leading-normal {
|
||||
line-height: 1.5;
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
import { Button, Col, Form, Row } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { t } from 'i18next';
|
||||
import { isUndefined, map, startCase } from 'lodash';
|
||||
import { isUndefined, map, omit, startCase } from 'lodash';
|
||||
import React, {
|
||||
FocusEvent,
|
||||
useCallback,
|
||||
@ -32,14 +32,12 @@ import {
|
||||
} from '../../../../constants/service-guide.constant';
|
||||
import { EntityType } from '../../../../enums/entity.enum';
|
||||
import { ServiceCategory } from '../../../../enums/service.enum';
|
||||
import {
|
||||
Category,
|
||||
CustomProperty,
|
||||
Type,
|
||||
} from '../../../../generated/entity/type';
|
||||
import { Category, Type } from '../../../../generated/entity/type';
|
||||
import { CustomProperty } from '../../../../generated/type/customProperty';
|
||||
import {
|
||||
FieldProp,
|
||||
FieldTypes,
|
||||
FormItemLayout,
|
||||
} from '../../../../interface/FormUtils.interface';
|
||||
import {
|
||||
addPropertyToEntity,
|
||||
@ -55,6 +53,7 @@ import ServiceDocPanel from '../../../common/ServiceDocPanel/ServiceDocPanel';
|
||||
import TitleBreadcrumb from '../../../common/TitleBreadcrumb/TitleBreadcrumb.component';
|
||||
|
||||
const AddCustomProperty = () => {
|
||||
const [form] = Form.useForm();
|
||||
const { entityType } = useParams<{ entityType: EntityType }>();
|
||||
const history = useHistory();
|
||||
|
||||
@ -64,6 +63,8 @@ const AddCustomProperty = () => {
|
||||
const [activeField, setActiveField] = useState<string>('');
|
||||
const [isCreating, setIsCreating] = useState<boolean>(false);
|
||||
|
||||
const watchedPropertyType = Form.useWatch('propertyType', form);
|
||||
|
||||
const slashedBreadcrumb = useMemo(
|
||||
() => [
|
||||
{
|
||||
@ -99,6 +100,10 @@ const AddCustomProperty = () => {
|
||||
}));
|
||||
}, [propertyTypes]);
|
||||
|
||||
const isEnumType =
|
||||
propertyTypeOptions.find((option) => option.value === watchedPropertyType)
|
||||
?.key === 'enum';
|
||||
|
||||
const fetchPropertyType = async () => {
|
||||
try {
|
||||
const response = await getTypeListByCategory(Category.Field);
|
||||
@ -130,7 +135,15 @@ const AddCustomProperty = () => {
|
||||
* In CustomProperty the propertyType is type of entity reference, however from the form we
|
||||
* get propertyType as string
|
||||
*/
|
||||
data: Exclude<CustomProperty, 'propertyType'> & { propertyType: string }
|
||||
/**
|
||||
* In CustomProperty the customPropertyConfig is type of CustomPropertyConfig, however from the
|
||||
* form we get customPropertyConfig as string[]
|
||||
*/
|
||||
data: Exclude<CustomProperty, 'propertyType' | 'customPropertyConfig'> & {
|
||||
propertyType: string;
|
||||
customPropertyConfig: string[];
|
||||
multiSelect?: boolean;
|
||||
}
|
||||
) => {
|
||||
if (isUndefined(typeDetail)) {
|
||||
return;
|
||||
@ -139,11 +152,22 @@ const AddCustomProperty = () => {
|
||||
try {
|
||||
setIsCreating(true);
|
||||
await addPropertyToEntity(typeDetail?.id ?? '', {
|
||||
...data,
|
||||
...omit(data, 'multiSelect'),
|
||||
propertyType: {
|
||||
id: data.propertyType,
|
||||
type: 'type',
|
||||
},
|
||||
// Only add customPropertyConfig if it is an enum type
|
||||
...(isEnumType
|
||||
? {
|
||||
customPropertyConfig: {
|
||||
config: {
|
||||
multiSelect: Boolean(data?.multiSelect),
|
||||
values: data.customPropertyConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
history.goBack();
|
||||
} catch (error) {
|
||||
@ -194,29 +218,73 @@ const AddCustomProperty = () => {
|
||||
})}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
required: true,
|
||||
label: t('label.description'),
|
||||
id: 'root/description',
|
||||
type: FieldTypes.DESCRIPTION,
|
||||
props: {
|
||||
'data-testid': 'description',
|
||||
initialValue: '',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const descriptionField: FieldProp = {
|
||||
name: 'description',
|
||||
required: true,
|
||||
label: t('label.description'),
|
||||
id: 'root/description',
|
||||
type: FieldTypes.DESCRIPTION,
|
||||
props: {
|
||||
'data-testid': 'description',
|
||||
initialValue: '',
|
||||
},
|
||||
};
|
||||
|
||||
const customPropertyConfigTypeValueField: FieldProp = {
|
||||
name: 'customPropertyConfig',
|
||||
required: false,
|
||||
label: t('label.enum-value-plural'),
|
||||
id: 'root/customPropertyConfig',
|
||||
type: FieldTypes.SELECT,
|
||||
props: {
|
||||
'data-testid': 'customPropertyConfig',
|
||||
mode: 'tags',
|
||||
placeholder: t('label.enum-value-plural'),
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: t('label.field-required', {
|
||||
field: t('label.enum-value-plural'),
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const multiSelectField: FieldProp = {
|
||||
name: 'multiSelect',
|
||||
label: t('label.multi-select'),
|
||||
type: FieldTypes.SWITCH,
|
||||
required: false,
|
||||
props: {
|
||||
'data-testid': 'multiSelect',
|
||||
},
|
||||
id: 'root/multiSelect',
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
};
|
||||
|
||||
const firstPanelChildren = (
|
||||
<div className="max-width-md w-9/10 service-form-container">
|
||||
<TitleBreadcrumb titleLinks={slashedBreadcrumb} />
|
||||
<Form
|
||||
className="m-t-md"
|
||||
data-testid="custom-property-form"
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
onFocus={handleFieldFocus}>
|
||||
{generateFormFields(formFields)}
|
||||
{isEnumType && (
|
||||
<>
|
||||
{generateFormFields([
|
||||
customPropertyConfigTypeValueField,
|
||||
multiSelectField,
|
||||
])}
|
||||
</>
|
||||
)}
|
||||
{generateFormFields([descriptionField])}
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button
|
||||
|
||||
@ -11,7 +11,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CustomProperty, Type } from '../../../generated/entity/type';
|
||||
import { Type } from '../../../generated/entity/type';
|
||||
import { CustomProperty } from '../../../generated/type/customProperty';
|
||||
|
||||
export interface CustomPropertyTableProp {
|
||||
hasAccess: boolean;
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
*/
|
||||
import { Button, Space, Tooltip, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { isEmpty, isString, isUndefined } from 'lodash';
|
||||
import React, { FC, Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as IconEdit } from '../../../assets/svg/edit-new.svg';
|
||||
@ -20,14 +20,16 @@ import { ReactComponent as IconDelete } from '../../../assets/svg/ic-delete.svg'
|
||||
import { ADD_CUSTOM_PROPERTIES_DOCS } from '../../../constants/docs.constants';
|
||||
import { NO_PERMISSION_FOR_ACTION } from '../../../constants/HelperTextUtil';
|
||||
import { ERROR_PLACEHOLDER_TYPE, OPERATION } from '../../../enums/common.enum';
|
||||
import { CustomProperty } from '../../../generated/entity/type';
|
||||
import { CustomProperty } from '../../../generated/type/customProperty';
|
||||
import { columnSorter, getEntityName } from '../../../utils/EntityUtils';
|
||||
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
import RichTextEditorPreviewer from '../../common/RichTextEditor/RichTextEditorPreviewer';
|
||||
import Table from '../../common/Table/Table';
|
||||
import ConfirmationModal from '../../Modals/ConfirmationModal/ConfirmationModal';
|
||||
import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
|
||||
import { CustomPropertyTableProp } from './CustomPropertyTable.interface';
|
||||
import EditCustomPropertyModal, {
|
||||
FormData,
|
||||
} from './EditCustomPropertyModal/EditCustomPropertyModal';
|
||||
|
||||
export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
|
||||
customProperties,
|
||||
@ -61,10 +63,23 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
|
||||
}
|
||||
}, [isButtonLoading]);
|
||||
|
||||
const handlePropertyUpdate = async (updatedDescription: string) => {
|
||||
const handlePropertyUpdate = async (data: FormData) => {
|
||||
const updatedProperties = customProperties.map((property) => {
|
||||
if (property.name === selectedProperty.name) {
|
||||
return { ...property, description: updatedDescription };
|
||||
return {
|
||||
...property,
|
||||
description: data.description,
|
||||
...(data.customPropertyConfig
|
||||
? {
|
||||
customPropertyConfig: {
|
||||
config: {
|
||||
multiSelect: Boolean(data?.multiSelect),
|
||||
values: data.customPropertyConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
} else {
|
||||
return property;
|
||||
}
|
||||
@ -97,6 +112,34 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
|
||||
key: 'propertyType',
|
||||
render: (text) => getEntityName(text),
|
||||
},
|
||||
{
|
||||
title: t('label.config'),
|
||||
dataIndex: 'customPropertyConfig',
|
||||
key: 'customPropertyConfig',
|
||||
render: (data: CustomProperty['customPropertyConfig']) => {
|
||||
if (isUndefined(data)) {
|
||||
return <span>--</span>;
|
||||
}
|
||||
|
||||
const config = data.config;
|
||||
|
||||
if (!isString(config)) {
|
||||
return (
|
||||
<Space data-testid="enum-config" direction="vertical" size={4}>
|
||||
<Typography.Text>
|
||||
{JSON.stringify(config?.values ?? [])}
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
{t('label.multi-select')}:{' '}
|
||||
{config?.multiSelect ? t('label.yes') : t('label.no')}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
return <Typography.Text>{config}</Typography.Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('label.description'),
|
||||
dataIndex: 'description',
|
||||
@ -204,19 +247,14 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
|
||||
onCancel={resetSelectedProperty}
|
||||
onConfirm={handlePropertyDelete}
|
||||
/>
|
||||
<ModalWithMarkdownEditor
|
||||
header={t('label.edit-entity-name', {
|
||||
entityType: t('label.property'),
|
||||
entityName: selectedProperty.name,
|
||||
})}
|
||||
placeholder={t('label.enter-field-description', {
|
||||
field: t('label.property'),
|
||||
})}
|
||||
value={selectedProperty.description || ''}
|
||||
visible={updateCheck}
|
||||
onCancel={resetSelectedProperty}
|
||||
onSave={handlePropertyUpdate}
|
||||
/>
|
||||
{updateCheck && (
|
||||
<EditCustomPropertyModal
|
||||
customProperty={selectedProperty}
|
||||
visible={updateCheck}
|
||||
onCancel={resetSelectedProperty}
|
||||
onSave={handlePropertyUpdate}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,179 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Form, Modal, Typography } from 'antd';
|
||||
import { isUndefined, uniq } from 'lodash';
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
CustomProperty,
|
||||
EnumConfig,
|
||||
} from '../../../../generated/type/customProperty';
|
||||
import {
|
||||
FieldProp,
|
||||
FieldTypes,
|
||||
FormItemLayout,
|
||||
} from '../../../../interface/FormUtils.interface';
|
||||
import { generateFormFields } from '../../../../utils/formUtils';
|
||||
|
||||
export interface FormData {
|
||||
description: string;
|
||||
customPropertyConfig: string[];
|
||||
multiSelect?: boolean;
|
||||
}
|
||||
|
||||
interface EditCustomPropertyModalProps {
|
||||
customProperty: CustomProperty;
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onSave: (data: FormData) => Promise<void>;
|
||||
}
|
||||
|
||||
const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
|
||||
customProperty,
|
||||
onCancel,
|
||||
visible,
|
||||
onSave,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
|
||||
const handleSubmit = async (data: FormData) => {
|
||||
setIsSaving(true);
|
||||
await onSave(data);
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const formFields: FieldProp[] = [
|
||||
{
|
||||
name: 'description',
|
||||
required: true,
|
||||
label: t('label.description'),
|
||||
id: 'root/description',
|
||||
type: FieldTypes.DESCRIPTION,
|
||||
props: {
|
||||
'data-testid': 'description',
|
||||
initialValue: customProperty.description,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const customPropertyConfigField: FieldProp = {
|
||||
name: 'customPropertyConfig',
|
||||
required: false,
|
||||
label: t('label.enum-value-plural'),
|
||||
id: 'root/customPropertyConfig',
|
||||
type: FieldTypes.SELECT,
|
||||
props: {
|
||||
'data-testid': 'customPropertyConfig',
|
||||
mode: 'tags',
|
||||
placeholder: t('label.enum-value-plural'),
|
||||
onChange: (value: string[]) => {
|
||||
const enumConfig = customProperty.customPropertyConfig
|
||||
?.config as EnumConfig;
|
||||
const updatedValues = uniq([...value, ...(enumConfig?.values ?? [])]);
|
||||
form.setFieldsValue({ customPropertyConfig: updatedValues });
|
||||
},
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: t('label.field-required', {
|
||||
field: t('label.enum-value-plural'),
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const multiSelectField: FieldProp = {
|
||||
name: 'multiSelect',
|
||||
label: t('label.multi-select'),
|
||||
type: FieldTypes.SWITCH,
|
||||
required: false,
|
||||
props: {
|
||||
'data-testid': 'multiSelect',
|
||||
},
|
||||
id: 'root/multiSelect',
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
};
|
||||
|
||||
const initialValues = useMemo(() => {
|
||||
const isEnumType = customProperty.propertyType.name === 'enum';
|
||||
if (isEnumType) {
|
||||
const enumConfig = customProperty.customPropertyConfig
|
||||
?.config as EnumConfig;
|
||||
|
||||
return {
|
||||
description: customProperty.description,
|
||||
customPropertyConfig: enumConfig?.values ?? [],
|
||||
multiSelect: Boolean(enumConfig?.multiSelect),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
description: customProperty.description,
|
||||
customPropertyConfig: customProperty.customPropertyConfig?.config,
|
||||
};
|
||||
}, [customProperty]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
centered
|
||||
destroyOnClose
|
||||
cancelButtonProps={{ disabled: isSaving }}
|
||||
closable={false}
|
||||
data-testid="edit-custom-property-modal"
|
||||
maskClosable={false}
|
||||
okButtonProps={{
|
||||
htmlType: 'submit',
|
||||
form: 'edit-custom-property-form',
|
||||
loading: isSaving,
|
||||
}}
|
||||
okText={t('label.save')}
|
||||
open={visible}
|
||||
title={
|
||||
<Typography.Text>
|
||||
{t('label.edit-entity-name', {
|
||||
entityType: t('label.property'),
|
||||
entityName: customProperty.name,
|
||||
})}
|
||||
</Typography.Text>
|
||||
}
|
||||
width={750}
|
||||
onCancel={onCancel}>
|
||||
<Form
|
||||
form={form}
|
||||
id="edit-custom-property-form"
|
||||
initialValues={initialValues}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}>
|
||||
{generateFormFields(formFields)}
|
||||
{!isUndefined(customProperty.customPropertyConfig) && (
|
||||
<>
|
||||
{generateFormFields([customPropertyConfigField])}
|
||||
<Typography.Text
|
||||
className="text-grey-muted"
|
||||
style={{ display: 'block', marginTop: '-18px' }}>
|
||||
{`Note: ${t(
|
||||
'message.updating-existing-not-possible-can-add-new-values'
|
||||
)}`}
|
||||
</Typography.Text>
|
||||
{generateFormFields([multiSelectField])}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditCustomPropertyModal;
|
||||
@ -24,11 +24,7 @@ import { CUSTOM_PROPERTIES_DOCS } from '../../../constants/docs.constants';
|
||||
import { EntityField } from '../../../constants/Feeds.constants';
|
||||
import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum';
|
||||
import { EntityTabs, EntityType } from '../../../enums/entity.enum';
|
||||
import {
|
||||
ChangeDescription,
|
||||
CustomProperty,
|
||||
Type,
|
||||
} from '../../../generated/entity/type';
|
||||
import { ChangeDescription, Type } from '../../../generated/entity/type';
|
||||
import { getTypeByFQN } from '../../../rest/metadataTypeAPI';
|
||||
|
||||
import { getEntityDetailLink, Transi18next } from '../../../utils/CommonUtils';
|
||||
@ -38,6 +34,7 @@ import {
|
||||
OperationPermission,
|
||||
ResourceEntity,
|
||||
} from '../../../context/PermissionProvider/PermissionProvider.interface';
|
||||
import { CustomProperty } from '../../../generated/type/customProperty';
|
||||
import { columnSorter, getEntityName } from '../../../utils/EntityUtils';
|
||||
import {
|
||||
getChangedEntityNewValue,
|
||||
@ -172,8 +169,7 @@ export const CustomPropertyTable = <T extends ExtentionEntitiesKeys>({
|
||||
extension={extensionObject.extensionObject}
|
||||
hasEditPermissions={hasEditAccess}
|
||||
isVersionView={isVersionView}
|
||||
propertyName={record.name}
|
||||
propertyType={record.propertyType}
|
||||
property={record}
|
||||
versionDataKeys={extensionObject.addedKeysList}
|
||||
onExtensionUpdate={onExtensionUpdate}
|
||||
/>
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { PropertyValue } from './PropertyValue';
|
||||
|
||||
@ -35,22 +35,27 @@ jest.mock(
|
||||
jest.mock('./PropertyInput', () => ({
|
||||
PropertyInput: jest
|
||||
.fn()
|
||||
.mockReturnValue(<div data-testid="PropertyInput">PropertyInput</div>),
|
||||
.mockImplementation(({ children }) => (
|
||||
<div data-testid="PropertyInput">{children}</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
const mockUpdate = jest.fn();
|
||||
|
||||
const mockData = {
|
||||
extension: { yNumber: 87 },
|
||||
propertyName: 'yNumber',
|
||||
propertyType: {
|
||||
id: '73f1e4a4-4c62-4399-9d6d-4a3906851483',
|
||||
type: 'type',
|
||||
name: 'integer',
|
||||
fullyQualifiedName: 'integer',
|
||||
description: '"An integer type."',
|
||||
displayName: 'integer',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/73f1e4a4-4c62-4399-9d6d-4a3906851483',
|
||||
property: {
|
||||
name: 'yNumber',
|
||||
propertyType: {
|
||||
id: '73f1e4a4-4c62-4399-9d6d-4a3906851483',
|
||||
type: 'type',
|
||||
name: 'integer',
|
||||
fullyQualifiedName: 'integer',
|
||||
description: '"An integer type."',
|
||||
displayName: 'integer',
|
||||
href: 'http://localhost:8585/api/v1/metadata/types/73f1e4a4-4c62-4399-9d6d-4a3906851483',
|
||||
},
|
||||
description: 'A number property.',
|
||||
},
|
||||
onExtensionUpdate: mockUpdate,
|
||||
hasEditPermissions: true,
|
||||
@ -66,7 +71,9 @@ describe('Test PropertyValue Component', () => {
|
||||
expect(valueElement).toBeInTheDocument();
|
||||
expect(iconElement).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(iconElement);
|
||||
await act(async () => {
|
||||
fireEvent.click(iconElement);
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('PropertyInput')).toBeInTheDocument();
|
||||
});
|
||||
@ -81,12 +88,15 @@ describe('Test PropertyValue Component', () => {
|
||||
|
||||
it('Should render richtext previewer component for markdown type', async () => {
|
||||
const extension = { yNumber: 'markdown value' };
|
||||
const propertyType = { ...mockData.propertyType, name: 'markdown' };
|
||||
const propertyType = {
|
||||
...mockData.property.propertyType,
|
||||
name: 'markdown',
|
||||
};
|
||||
render(
|
||||
<PropertyValue
|
||||
{...mockData}
|
||||
extension={extension}
|
||||
propertyType={propertyType}
|
||||
property={{ ...mockData.property, propertyType: propertyType }}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -96,8 +106,33 @@ describe('Test PropertyValue Component', () => {
|
||||
expect(valueElement).toBeInTheDocument();
|
||||
expect(iconElement).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(iconElement);
|
||||
await act(async () => {
|
||||
fireEvent.click(iconElement);
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('EditorModal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render select component for enum type', async () => {
|
||||
const extension = { yNumber: 'enumValue' };
|
||||
const propertyType = {
|
||||
...mockData.property.propertyType,
|
||||
name: 'enum',
|
||||
};
|
||||
render(
|
||||
<PropertyValue
|
||||
{...mockData}
|
||||
extension={extension}
|
||||
property={{ ...mockData.property, propertyType: propertyType }}
|
||||
/>
|
||||
);
|
||||
|
||||
const iconElement = await screen.findByTestId('edit-icon');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(iconElement);
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('enum-select')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@ -12,25 +12,28 @@
|
||||
*/
|
||||
|
||||
import Icon from '@ant-design/icons';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import { Form, Select, Tooltip, Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { t } from 'i18next';
|
||||
import { isUndefined, toNumber } from 'lodash';
|
||||
import { isArray, isEmpty, isUndefined, noop, toNumber } from 'lodash';
|
||||
import React, { FC, Fragment, useState } from 'react';
|
||||
import { ReactComponent as EditIconComponent } from '../../../assets/svg/edit-new.svg';
|
||||
import { DE_ACTIVE_COLOR, ICON_DIMENSION } from '../../../constants/constants';
|
||||
import { Table } from '../../../generated/entity/data/table';
|
||||
import { EntityReference } from '../../../generated/type/entityReference';
|
||||
import {
|
||||
CustomProperty,
|
||||
EnumConfig,
|
||||
} from '../../../generated/type/customProperty';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
|
||||
import InlineEdit from '../InlineEdit/InlineEdit.component';
|
||||
import RichTextEditorPreviewer from '../RichTextEditor/RichTextEditorPreviewer';
|
||||
import { PropertyInput } from './PropertyInput';
|
||||
|
||||
interface Props {
|
||||
versionDataKeys?: string[];
|
||||
isVersionView?: boolean;
|
||||
propertyName: string;
|
||||
propertyType: EntityReference;
|
||||
property: CustomProperty;
|
||||
extension: Table['extension'];
|
||||
onExtensionUpdate: (updatedExtension: Table['extension']) => Promise<void>;
|
||||
hasEditPermissions: boolean;
|
||||
@ -39,12 +42,14 @@ interface Props {
|
||||
export const PropertyValue: FC<Props> = ({
|
||||
isVersionView,
|
||||
versionDataKeys,
|
||||
propertyName,
|
||||
extension,
|
||||
propertyType,
|
||||
onExtensionUpdate,
|
||||
hasEditPermissions,
|
||||
property,
|
||||
}) => {
|
||||
const propertyName = property.name;
|
||||
const propertyType = property.propertyType;
|
||||
|
||||
const value = extension?.[propertyName];
|
||||
|
||||
const [showInput, setShowInput] = useState<boolean>(false);
|
||||
@ -58,14 +63,18 @@ export const PropertyValue: FC<Props> = ({
|
||||
setShowInput(false);
|
||||
};
|
||||
|
||||
const onInputSave = async (updatedValue: string | number) => {
|
||||
const onInputSave = async (updatedValue: string | number | string[]) => {
|
||||
const isEnum = propertyType.name === 'enum';
|
||||
const isArrayType = isArray(updatedValue);
|
||||
const enumValue = isArrayType ? updatedValue : [updatedValue];
|
||||
const propertyValue = isEnum ? enumValue : updatedValue;
|
||||
try {
|
||||
const updatedExtension = {
|
||||
...(extension || {}),
|
||||
[propertyName]:
|
||||
propertyType.name === 'integer'
|
||||
? toNumber(updatedValue || 0)
|
||||
: updatedValue,
|
||||
: propertyValue,
|
||||
};
|
||||
setIsLoading(true);
|
||||
await onExtensionUpdate(updatedExtension);
|
||||
@ -105,6 +114,57 @@ export const PropertyValue: FC<Props> = ({
|
||||
onSave={onInputSave}
|
||||
/>
|
||||
);
|
||||
case 'enum': {
|
||||
const enumConfig = property.customPropertyConfig?.config as EnumConfig;
|
||||
const isMultiSelect = Boolean(enumConfig?.multiSelect);
|
||||
const options = enumConfig?.values?.map((option) => ({
|
||||
label: option,
|
||||
value: option,
|
||||
}));
|
||||
|
||||
return (
|
||||
<InlineEdit
|
||||
isLoading={isLoading}
|
||||
saveButtonProps={{
|
||||
disabled: isLoading,
|
||||
htmlType: 'submit',
|
||||
form: 'enum-form',
|
||||
}}
|
||||
onCancel={onHideInput}
|
||||
onSave={noop}>
|
||||
<Form
|
||||
id="enum-form"
|
||||
initialValues={{
|
||||
enumValues: (isArray(value) ? value : [value]).filter(Boolean),
|
||||
}}
|
||||
layout="vertical"
|
||||
onFinish={(values: { enumValues: string | string[] }) =>
|
||||
onInputSave(values.enumValues)
|
||||
}>
|
||||
<Form.Item
|
||||
name="enumValues"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('label.field-required', {
|
||||
field: t('label.enum-value-plural'),
|
||||
}),
|
||||
},
|
||||
]}
|
||||
style={{ marginBottom: '0px' }}>
|
||||
<Select
|
||||
data-testid="enum-select"
|
||||
disabled={isLoading}
|
||||
mode={isMultiSelect ? 'multiple' : undefined}
|
||||
options={options}
|
||||
placeholder={t('label.enum-value-plural')}
|
||||
style={{ width: '250px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</InlineEdit>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
@ -126,6 +186,13 @@ export const PropertyValue: FC<Props> = ({
|
||||
case 'markdown':
|
||||
return <RichTextEditorPreviewer markdown={value || ''} />;
|
||||
|
||||
case 'enum':
|
||||
return (
|
||||
<Typography.Text className="break-all" data-testid="value">
|
||||
{isArray(value) ? value.join(', ') : value}
|
||||
</Typography.Text>
|
||||
);
|
||||
|
||||
case 'string':
|
||||
case 'integer':
|
||||
default:
|
||||
@ -149,7 +216,7 @@ export const PropertyValue: FC<Props> = ({
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return value ? (
|
||||
return !isEmpty(value) ? (
|
||||
propertyValue
|
||||
) : (
|
||||
<span className="text-grey-muted" data-testid="no-data">
|
||||
|
||||
@ -23,6 +23,8 @@ const InlineEdit = ({
|
||||
direction,
|
||||
className,
|
||||
isLoading,
|
||||
cancelButtonProps,
|
||||
saveButtonProps,
|
||||
}: InlineEditProps) => {
|
||||
return (
|
||||
<Space
|
||||
@ -39,6 +41,7 @@ const InlineEdit = ({
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={onCancel}
|
||||
{...cancelButtonProps}
|
||||
/>
|
||||
<Button
|
||||
data-testid="inline-save-btn"
|
||||
@ -47,6 +50,7 @@ const InlineEdit = ({
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={onSave}
|
||||
{...saveButtonProps}
|
||||
/>
|
||||
</Space>
|
||||
</Space>
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { SpaceProps } from 'antd';
|
||||
import { ButtonProps, SpaceProps } from 'antd';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export interface InlineEditProps {
|
||||
@ -21,4 +21,6 @@ export interface InlineEditProps {
|
||||
onSave: () => void | Promise<void>;
|
||||
direction?: SpaceProps['direction'];
|
||||
isLoading?: boolean;
|
||||
cancelButtonProps?: ButtonProps;
|
||||
saveButtonProps?: ButtonProps;
|
||||
}
|
||||
|
||||
@ -47,7 +47,7 @@ export const LIGHT_GREEN_COLOR = '#4CAF50';
|
||||
export const DEFAULT_CHART_OPACITY = 1;
|
||||
export const HOVER_CHART_OPACITY = 0.3;
|
||||
|
||||
export const SUPPORTED_FIELD_TYPES = ['string', 'markdown', 'integer'];
|
||||
export const SUPPORTED_FIELD_TYPES = ['string', 'markdown', 'integer', 'enum'];
|
||||
export const LOGGED_IN_USER_STORAGE_KEY = 'loggedInUsers';
|
||||
export const ACTIVE_DOMAIN_STORAGE_KEY = 'activeDomain';
|
||||
export const DEFAULT_DOMAIN_VALUE = 'All Domains';
|
||||
|
||||
@ -414,6 +414,7 @@
|
||||
"entity-service": "{{entity}}-Dienst",
|
||||
"entity-type-plural": "{{entity}}-Typen",
|
||||
"entity-version-detail-plural": "Details zu {{entity}}-Versionen",
|
||||
"enum-value-plural": "Enum Values",
|
||||
"error": "Error",
|
||||
"error-plural": "Errors",
|
||||
"event-publisher-plural": "Ereignisveröffentlicher",
|
||||
@ -692,6 +693,7 @@
|
||||
"move-the-entity": "{{entity}} verschieben",
|
||||
"ms": "Milliseconds",
|
||||
"ms-team-plural": "MS-Teams",
|
||||
"multi-select": "Multi Select",
|
||||
"mutually-exclusive": "Gegenseitig ausschließend",
|
||||
"my-data": "Meine Daten",
|
||||
"name": "Name",
|
||||
@ -1723,6 +1725,7 @@
|
||||
"update-displayName-entity": "Aktualisieren Sie die Anzeigenamen für das {{entity}}.",
|
||||
"update-profiler-settings": "Update profiler setting.",
|
||||
"update-tag-message": "Request to update tags for",
|
||||
"updating-existing-not-possible-can-add-new-values": "Updating existing values is not possible,only the addition of new values is allowed.",
|
||||
"upstream-depth-message": "Bitte wählen Sie einen Wert für die Upstream-Tiefe aus",
|
||||
"upstream-depth-tooltip": "Zeigen Sie bis zu 3 Knoten der Upstream-Lineage an, um die Quelle (Elternebenen) zu identifizieren.",
|
||||
"usage-ingestion-description": "Die Nutzungsinhalation kann nach Einrichtung einer Metadateninhalation konfiguriert und bereitgestellt werden. Der Nutzungsinhalationsworkflow erhält die Abfrageprotokolle und Details zur Tabellenerstellung aus der zugrunde liegenden Datenbank und gibt sie an OpenMetadata weiter. Metadaten und Nutzung können nur eine Pipeline für einen Datenbankdienst haben. Legen Sie die Dauer des Abfrageprotokolls (in Tagen), den Speicherort der Staging-Datei und das Ergebnislimit fest, um zu beginnen.",
|
||||
|
||||
@ -414,6 +414,7 @@
|
||||
"entity-service": "{{entity}} Service",
|
||||
"entity-type-plural": "{{entity}} Type",
|
||||
"entity-version-detail-plural": "{{entity}} Version Details",
|
||||
"enum-value-plural": "Enum Values",
|
||||
"error": "Error",
|
||||
"error-plural": "Errors",
|
||||
"event-publisher-plural": "Event Publishers",
|
||||
@ -692,6 +693,7 @@
|
||||
"move-the-entity": "Move the {{entity}}",
|
||||
"ms": "Milliseconds",
|
||||
"ms-team-plural": "MS Teams",
|
||||
"multi-select": "Multi Select",
|
||||
"mutually-exclusive": "Mutually Exclusive",
|
||||
"my-data": "My Data",
|
||||
"name": "Name",
|
||||
@ -1723,6 +1725,7 @@
|
||||
"update-displayName-entity": "Update Display Name for the {{entity}}.",
|
||||
"update-profiler-settings": "Update profiler setting.",
|
||||
"update-tag-message": "Request to update tags for",
|
||||
"updating-existing-not-possible-can-add-new-values": "Updating existing values is not possible,only the addition of new values is allowed.",
|
||||
"upstream-depth-message": "Please select a value for upstream depth",
|
||||
"upstream-depth-tooltip": "Display up to 3 nodes of upstream lineage to identify the source (parent levels).",
|
||||
"usage-ingestion-description": "Usage ingestion can be configured and deployed after a metadata ingestion has been set up. The usage ingestion workflow obtains the query log and table creation details from the underlying database and feeds it to OpenMetadata. Metadata and usage can have only one pipeline for a database service. Define the Query Log Duration (in days), Stage File Location, and Result Limit to start.",
|
||||
|
||||
@ -414,6 +414,7 @@
|
||||
"entity-service": "Servicio de {{entity}}",
|
||||
"entity-type-plural": "{{entity}} Type",
|
||||
"entity-version-detail-plural": "{{entity}} Version Details",
|
||||
"enum-value-plural": "Enum Values",
|
||||
"error": "Error",
|
||||
"error-plural": "Errors",
|
||||
"event-publisher-plural": "Publicadores de eventos",
|
||||
@ -692,6 +693,7 @@
|
||||
"move-the-entity": "Mover la {{entity}}",
|
||||
"ms": "Milliseconds",
|
||||
"ms-team-plural": "Equipos de MS",
|
||||
"multi-select": "Multi Select",
|
||||
"mutually-exclusive": "Mutuamente Exclusivo",
|
||||
"my-data": "Mis Datos",
|
||||
"name": "Nombre",
|
||||
@ -1723,6 +1725,7 @@
|
||||
"update-displayName-entity": "Update Display Name for the {{entity}}.",
|
||||
"update-profiler-settings": "Update profiler setting.",
|
||||
"update-tag-message": "Request to update tags for",
|
||||
"updating-existing-not-possible-can-add-new-values": "Updating existing values is not possible,only the addition of new values is allowed.",
|
||||
"upstream-depth-message": "Seleccione un valor para la profundidad ascendente",
|
||||
"upstream-depth-tooltip": "Muestra hasta 3 nodos de la línea ascendente para identificar la fuente (niveles padre).",
|
||||
"usage-ingestion-description": "La ingesta de uso se puede configurar e implementar después de que se haya establecido una ingesta de metadatos. El flujo de trabajo de ingesta de uso obtiene el registro de consultas y los detalles de creación de tablas de la base de datos subyacente y los alimenta a OpenMetadata. Los metadatos y el uso solo pueden tener un workflow para un servicio de base de datos. Defina la duración del registro de consultas (en días), la ubicación del archivo de etapa y el límite de resultados para comenzar.",
|
||||
|
||||
@ -414,6 +414,7 @@
|
||||
"entity-service": "Service de {{entity}}",
|
||||
"entity-type-plural": "{{entity}} Types",
|
||||
"entity-version-detail-plural": "Détails des Versions de {{entity}}",
|
||||
"enum-value-plural": "Enum Values",
|
||||
"error": "Error",
|
||||
"error-plural": "Errors",
|
||||
"event-publisher-plural": "Publicateurs d'Événements",
|
||||
@ -692,6 +693,7 @@
|
||||
"move-the-entity": "Déplacer {{entity}}",
|
||||
"ms": "Milliseconds",
|
||||
"ms-team-plural": "Équipes MS",
|
||||
"multi-select": "Multi Select",
|
||||
"mutually-exclusive": "Mutuellement Exclusif",
|
||||
"my-data": "Mes Données",
|
||||
"name": "Nom",
|
||||
@ -1723,6 +1725,7 @@
|
||||
"update-displayName-entity": "Mettre à Jour le Nom d'Affichage de {{entity}}.",
|
||||
"update-profiler-settings": "Update profiler setting.",
|
||||
"update-tag-message": "Request to update tags for",
|
||||
"updating-existing-not-possible-can-add-new-values": "Updating existing values is not possible,only the addition of new values is allowed.",
|
||||
"upstream-depth-message": "Merci de sélectionner une valeur pour la profondeur amont",
|
||||
"upstream-depth-tooltip": "Afficher jusqu'à 3 nœuds de lignée en amont pour identifier la source (niveaux parentaux).",
|
||||
"usage-ingestion-description": "L'ingestion d'utilisation peut être configurée et déployée après qu'une ingestion de métadonnées a été configurée. Le flux de travail d'ingestion d'utilisation obtient le journal de requêtes et les détails de création de table à partir de la base de données sous-jacente et les transmet à OpenMetadata. Les métadonnées et l'utilisation ne peuvent avoir qu'un seul pipeline pour un service de base de données. Définissez la durée du journal de requêtes (en jours), l'emplacement du fichier de scène et la limite de résultat pour démarrer.",
|
||||
|
||||
@ -414,6 +414,7 @@
|
||||
"entity-service": "שירות {{entity}}",
|
||||
"entity-type-plural": "סוגי {{entity}}",
|
||||
"entity-version-detail-plural": "גרסאות פרטי {{entity}}",
|
||||
"enum-value-plural": "Enum Values",
|
||||
"error": "Error",
|
||||
"error-plural": "Errors",
|
||||
"event-publisher-plural": "מפרסמי אירועים",
|
||||
@ -692,6 +693,7 @@
|
||||
"move-the-entity": "העבר את {{entity}}",
|
||||
"ms": "מילי-שנייה",
|
||||
"ms-team-plural": "צוותי MS",
|
||||
"multi-select": "Multi Select",
|
||||
"mutually-exclusive": "באופן בלעדי",
|
||||
"my-data": "הנתונים שלי",
|
||||
"name": "שם",
|
||||
@ -1723,6 +1725,7 @@
|
||||
"update-displayName-entity": "עדכן את השם המוצג עבור {{entity}}.",
|
||||
"update-profiler-settings": "עדכן הגדרות הפרופיילר.",
|
||||
"update-tag-message": "Request to update tags for",
|
||||
"updating-existing-not-possible-can-add-new-values": "Updating existing values is not possible,only the addition of new values is allowed.",
|
||||
"upstream-depth-message": "יש לבחור ערך עבור עומק מערכת השואבת",
|
||||
"upstream-depth-tooltip": "הצג עד 3 צמתים של יחידת יורשת לזיהוי מקור (רמות ההורים).",
|
||||
"usage-ingestion-description": "ניתן להגדיר ולהפעיל את תהליך הספקת השימוש לאחר הגדרת השקפץ של מטה-דאטה. תהליך ספקת השימוש מקבל את לוגי השאילתות ופרטי יצירת הטבלה מהמסד נתונים התחתי ומספק אותם ל-OpenMetadata. יש רק פייפל אחד לשירות מסד נתונים. יש להגדיר משך לוגי השאילתות (בימים), מקום שמירת הקובץ הביניים, ומגבלת תוצאות להתחלה.",
|
||||
|
||||
@ -414,6 +414,7 @@
|
||||
"entity-service": "{{entity}}サービス",
|
||||
"entity-type-plural": "{{entity}} Type",
|
||||
"entity-version-detail-plural": "{{entity}} Version Details",
|
||||
"enum-value-plural": "Enum Values",
|
||||
"error": "Error",
|
||||
"error-plural": "Errors",
|
||||
"event-publisher-plural": "イベントの作成者",
|
||||
@ -692,6 +693,7 @@
|
||||
"move-the-entity": "Move the {{entity}}",
|
||||
"ms": "Milliseconds",
|
||||
"ms-team-plural": "MS Teams",
|
||||
"multi-select": "Multi Select",
|
||||
"mutually-exclusive": "Mutually Exclusive",
|
||||
"my-data": "マイデータ",
|
||||
"name": "名前",
|
||||
@ -1723,6 +1725,7 @@
|
||||
"update-displayName-entity": "Update Display Name for the {{entity}}.",
|
||||
"update-profiler-settings": "Update profiler setting.",
|
||||
"update-tag-message": "Request to update tags for",
|
||||
"updating-existing-not-possible-can-add-new-values": "Updating existing values is not possible,only the addition of new values is allowed.",
|
||||
"upstream-depth-message": "Please select a value for upstream depth",
|
||||
"upstream-depth-tooltip": "Display up to 3 nodes of upstream lineage to identify the source (parent levels).",
|
||||
"usage-ingestion-description": "Usage ingestion can be configured and deployed after a metadata ingestion has been set up. The usage ingestion workflow obtains the query log and table creation details from the underlying database and feeds it to OpenMetadata. Metadata and usage can have only one pipeline for a database service. Define the Query Log Duration (in days), Stage File Location, and Result Limit to start.",
|
||||
|
||||
@ -414,6 +414,7 @@
|
||||
"entity-service": "{{entity}}-service",
|
||||
"entity-type-plural": "{{entity}}-type",
|
||||
"entity-version-detail-plural": "{{entity}}-versie-details",
|
||||
"enum-value-plural": "Enum Values",
|
||||
"error": "Fout",
|
||||
"error-plural": "Errors",
|
||||
"event-publisher-plural": "Gebeurtenis-publisher",
|
||||
@ -692,6 +693,7 @@
|
||||
"move-the-entity": "Verplaats de {{entity}}",
|
||||
"ms": "Milliseconden",
|
||||
"ms-team-plural": "MS-teams",
|
||||
"multi-select": "Multi Select",
|
||||
"mutually-exclusive": "Wederzijds exclusief",
|
||||
"my-data": "Mijn data",
|
||||
"name": "Naam",
|
||||
@ -1723,6 +1725,7 @@
|
||||
"update-displayName-entity": "Update de weergavenaam voor de {{entity}}.",
|
||||
"update-profiler-settings": "Profielinstellingen updaten.",
|
||||
"update-tag-message": "Verzoek om de tags aan te passen voor",
|
||||
"updating-existing-not-possible-can-add-new-values": "Updating existing values is not possible,only the addition of new values is allowed.",
|
||||
"upstream-depth-message": "Selecteer een waarde voor upstream-diepte",
|
||||
"upstream-depth-tooltip": "Toon tot 3 knooppunten van upstream-lineage om de bron (parent-levels) te identificeren.",
|
||||
"usage-ingestion-description": "Gebruiksingestie kan worden geconfigureerd en ingezet nadat een metadataingestie is opgezet. De gebruiksingestieworkflow verkrijgt de querylogboeken en tabelcreatiedata van de onderliggende database en voedt deze naar OpenMetadata. Metadata en gebruik kunnen slechts één pijplijn hebben voor een databaseservice. Definieer de duur van het querylogboek (in dagen), de locatie van het stagingbestand en het resultatenlimiet om te beginnen.",
|
||||
|
||||
@ -414,6 +414,7 @@
|
||||
"entity-service": "Serviço de {{entity}}",
|
||||
"entity-type-plural": "Tipo de {{entity}}",
|
||||
"entity-version-detail-plural": "Detalhes da Versão de {{entity}}",
|
||||
"enum-value-plural": "Enum Values",
|
||||
"error": "Error",
|
||||
"error-plural": "Errors",
|
||||
"event-publisher-plural": "Publicadores de Eventos",
|
||||
@ -692,6 +693,7 @@
|
||||
"move-the-entity": "Mover a {{entity}}",
|
||||
"ms": "Milissegundos",
|
||||
"ms-team-plural": "MS Teams",
|
||||
"multi-select": "Multi Select",
|
||||
"mutually-exclusive": "Mutuamente Exclusivos",
|
||||
"my-data": "Meus Dados",
|
||||
"name": "Nome",
|
||||
@ -1723,6 +1725,7 @@
|
||||
"update-displayName-entity": "Atualizar o Nome de Exibição para {{entity}}.",
|
||||
"update-profiler-settings": "Atualizar configurações do examinador.",
|
||||
"update-tag-message": "Solicitar atualização de tags para",
|
||||
"updating-existing-not-possible-can-add-new-values": "Updating existing values is not possible,only the addition of new values is allowed.",
|
||||
"upstream-depth-message": "Por favor, selecione um valor para a profundidade a montante",
|
||||
"upstream-depth-tooltip": "Exibir até 3 nós de linhagem a montante para identificar a origem (níveis parentais).",
|
||||
"usage-ingestion-description": "A ingestão de uso pode ser configurada e implantada após a ingestão de metadados ter sido configurada. O fluxo de trabalho de ingestão de uso obtém o registro de consultas e os detalhes da criação de tabelas do banco de dados subjacente e os alimenta no OpenMetadata. Metadados e uso podem ter apenas um pipeline para um serviço de banco de dados. Defina a Duração do Registro de Consultas (em dias), o Local do Arquivo de Estágio e o Limite de Resultados para começar.",
|
||||
|
||||
@ -414,6 +414,7 @@
|
||||
"entity-service": "Сервис {{entity}}",
|
||||
"entity-type-plural": "Тип {{entity}}",
|
||||
"entity-version-detail-plural": "{{entity}} Version Details",
|
||||
"enum-value-plural": "Enum Values",
|
||||
"error": "Error",
|
||||
"error-plural": "Errors",
|
||||
"event-publisher-plural": "Издатели события",
|
||||
@ -692,6 +693,7 @@
|
||||
"move-the-entity": "Переместите {{entity}}",
|
||||
"ms": "Milliseconds",
|
||||
"ms-team-plural": "MS Команды",
|
||||
"multi-select": "Multi Select",
|
||||
"mutually-exclusive": "Единичный выбор",
|
||||
"my-data": "Мои данные",
|
||||
"name": "Наименование",
|
||||
@ -1723,6 +1725,7 @@
|
||||
"update-displayName-entity": "Обновите отображаемое имя для {{entity}}.",
|
||||
"update-profiler-settings": "Update profiler setting.",
|
||||
"update-tag-message": "Request to update tags for",
|
||||
"updating-existing-not-possible-can-add-new-values": "Updating existing values is not possible,only the addition of new values is allowed.",
|
||||
"upstream-depth-message": "Пожалуйста, выберите значение для восходящей линии",
|
||||
"upstream-depth-tooltip": "Отображение до 3 узлов восходящей линии для идентификации источника (родительские уровни).",
|
||||
"usage-ingestion-description": "Получение данных об использовании можно настроить и развернуть после настройки приема метаданных. Рабочий процесс приема данных об использовании получает журнал запросов и сведения о создании таблицы из базовой базы данных и передает их в OpenMetadata. Метаданные и использование могут иметь только один конвейер для сервиса базы данных. Определите продолжительность журнала запросов (в днях), расположение промежуточного файла и предел результатов для запуска.",
|
||||
|
||||
@ -414,6 +414,7 @@
|
||||
"entity-service": "{{entity}}服务",
|
||||
"entity-type-plural": "{{entity}}类型",
|
||||
"entity-version-detail-plural": "{{entity}}版本详情",
|
||||
"enum-value-plural": "Enum Values",
|
||||
"error": "Error",
|
||||
"error-plural": "Errors",
|
||||
"event-publisher-plural": "事件发布者",
|
||||
@ -692,6 +693,7 @@
|
||||
"move-the-entity": "移动{{entity}}",
|
||||
"ms": "Milliseconds",
|
||||
"ms-team-plural": "MS Teams",
|
||||
"multi-select": "Multi Select",
|
||||
"mutually-exclusive": "互斥的",
|
||||
"my-data": "我的数据",
|
||||
"name": "名称",
|
||||
@ -1723,6 +1725,7 @@
|
||||
"update-displayName-entity": "更改{{entity}}的显示名",
|
||||
"update-profiler-settings": "Update profiler setting.",
|
||||
"update-tag-message": "Request to update tags for",
|
||||
"updating-existing-not-possible-can-add-new-values": "Updating existing values is not possible,only the addition of new values is allowed.",
|
||||
"upstream-depth-message": "请选择一个上游深度的值",
|
||||
"upstream-depth-tooltip": "显示最多三个上游血缘节点以确定源(父级别)",
|
||||
"usage-ingestion-description": "在设置了元数据提取后,可以配置和部署使用率提取。使用率提取工作流从底层数据库获取查询日志和表创建详细信息,并将其提供给 OpenMetadata。一个数据库服务只能有一个使用率统计流水线。请通过定义查询日志持续时间(天)、临时文件位置和结果限制开始提取。",
|
||||
|
||||
@ -13,7 +13,8 @@
|
||||
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { Category, CustomProperty, Type } from '../generated/entity/type';
|
||||
import { Category, Type } from '../generated/entity/type';
|
||||
import { CustomProperty } from '../generated/type/customProperty';
|
||||
import { Paging } from '../generated/type/paging';
|
||||
import { getEncodedFqn } from '../utils/StringsUtils';
|
||||
import APIClient from './index';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user