mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-13 16:46:59 +00:00
Feature : Add table-type custom property (#18135)
This commit is contained in:
parent
2ef829052d
commit
1252698869
@ -320,11 +320,7 @@ public final class CsvUtil {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (list.get(0) instanceof Map && isEnumWithDescriptions((Map<String, Object>) list.get(0))) {
|
||||
return list.stream()
|
||||
.map(item -> ((Map<String, Object>) item).get("key").toString())
|
||||
.collect(Collectors.joining(INTERNAL_ARRAY_SEPARATOR));
|
||||
} else if (list.get(0) instanceof Map) {
|
||||
if (list.get(0) instanceof Map) {
|
||||
return list.stream()
|
||||
.map(item -> formatMapValue((Map<String, Object>) item))
|
||||
.collect(Collectors.joining(INTERNAL_ARRAY_SEPARATOR));
|
||||
@ -343,10 +339,6 @@ public final class CsvUtil {
|
||||
return valueMap.containsKey("start") && valueMap.containsKey("end");
|
||||
}
|
||||
|
||||
private static boolean isEnumWithDescriptions(Map<String, Object> valueMap) {
|
||||
return valueMap.containsKey("key") && valueMap.containsKey("description");
|
||||
}
|
||||
|
||||
private static String formatEntityReference(Map<String, Object> valueMap) {
|
||||
return valueMap.get("type") + ENTITY_TYPE_SEPARATOR + valueMap.get("fullyQualifiedName");
|
||||
}
|
||||
|
||||
@ -46,8 +46,6 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
import javax.ws.rs.core.Response;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.csv.CSVFormat;
|
||||
@ -68,7 +66,6 @@ import org.openmetadata.schema.type.csv.CsvErrorType;
|
||||
import org.openmetadata.schema.type.csv.CsvFile;
|
||||
import org.openmetadata.schema.type.csv.CsvHeader;
|
||||
import org.openmetadata.schema.type.csv.CsvImportResult;
|
||||
import org.openmetadata.schema.type.customproperties.EnumWithDescriptionsConfig;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.TypeRegistry;
|
||||
import org.openmetadata.service.jdbi3.EntityRepository;
|
||||
@ -408,16 +405,6 @@ public abstract class EntityCsv<T extends EntityInterface> {
|
||||
fieldValue = null;
|
||||
}
|
||||
}
|
||||
case "enumWithDescriptions" -> {
|
||||
fieldValue =
|
||||
parseEnumWithDescriptions(
|
||||
printer,
|
||||
csvRecord,
|
||||
fieldNumber,
|
||||
fieldName,
|
||||
fieldValue.toString(),
|
||||
propertyConfig);
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
// Validate the field against the JSON schema
|
||||
@ -461,70 +448,6 @@ public abstract class EntityCsv<T extends EntityInterface> {
|
||||
return isList ? entityReferences : entityReferences.isEmpty() ? null : entityReferences.get(0);
|
||||
}
|
||||
|
||||
private Object parseEnumWithDescriptions(
|
||||
CSVPrinter printer,
|
||||
CSVRecord csvRecord,
|
||||
int fieldNumber,
|
||||
String fieldName,
|
||||
String fieldValue,
|
||||
String propertyConfig)
|
||||
throws IOException {
|
||||
List<String> enumKeys = listOrEmpty(fieldToInternalArray(fieldValue));
|
||||
List<Object> enumObjects = new ArrayList<>();
|
||||
|
||||
JsonNode propertyConfigNode = JsonUtils.readTree(propertyConfig);
|
||||
if (propertyConfigNode == null) {
|
||||
importFailure(
|
||||
printer,
|
||||
invalidCustomPropertyFieldFormat(
|
||||
fieldNumber,
|
||||
fieldName,
|
||||
"enumWithDescriptions",
|
||||
"Invalid propertyConfig of enumWithDescriptions: " + fieldValue),
|
||||
csvRecord);
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, JsonNode> keyToObjectMap =
|
||||
StreamSupport.stream(propertyConfigNode.get("values").spliterator(), false)
|
||||
.collect(Collectors.toMap(node -> node.get("key").asText(), node -> node));
|
||||
EnumWithDescriptionsConfig config =
|
||||
JsonUtils.treeToValue(propertyConfigNode, EnumWithDescriptionsConfig.class);
|
||||
if (!config.getMultiSelect() && enumKeys.size() > 1) {
|
||||
importFailure(
|
||||
printer,
|
||||
invalidCustomPropertyFieldFormat(
|
||||
fieldNumber,
|
||||
fieldName,
|
||||
"enumWithDescriptions",
|
||||
"only one key is allowed for non-multiSelect enumWithDescriptions"),
|
||||
csvRecord);
|
||||
return null;
|
||||
}
|
||||
|
||||
for (String key : enumKeys) {
|
||||
try {
|
||||
JsonNode valueObject = keyToObjectMap.get(key);
|
||||
if (valueObject == null) {
|
||||
importFailure(
|
||||
printer,
|
||||
invalidCustomPropertyValue(
|
||||
fieldNumber,
|
||||
fieldName,
|
||||
"enumWithDescriptions",
|
||||
key + " not found in propertyConfig of " + fieldName),
|
||||
csvRecord);
|
||||
return null;
|
||||
}
|
||||
enumObjects.add(valueObject);
|
||||
} catch (Exception e) {
|
||||
importFailure(printer, e.getMessage(), csvRecord);
|
||||
}
|
||||
}
|
||||
|
||||
return enumObjects;
|
||||
}
|
||||
|
||||
protected String getFormattedDateTimeField(
|
||||
CSVPrinter printer,
|
||||
CSVRecord csvRecord,
|
||||
|
||||
@ -253,6 +253,10 @@ public final class CatalogExceptionMessage {
|
||||
return String.format("Custom field %s has invalid JSON %s", fieldName, validationMessages);
|
||||
}
|
||||
|
||||
public static String customPropertyConfigError(String fieldName, String validationMessages) {
|
||||
return String.format("Custom Property %s has invalid value %s", fieldName, validationMessages);
|
||||
}
|
||||
|
||||
public static String invalidParent(Team parent, String child, TeamType childType) {
|
||||
return String.format(
|
||||
"Team %s of type %s can't be of parent of team %s of type %s",
|
||||
|
||||
@ -67,7 +67,6 @@ 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;
|
||||
@ -101,8 +100,8 @@ 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.ConstraintViolationException;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
@ -147,7 +146,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.type.customproperties.TableConfig;
|
||||
import org.openmetadata.schema.utils.EntityInterfaceUtil;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.OpenMetadataApplicationConfig;
|
||||
@ -1493,8 +1492,7 @@ public abstract class EntityRepository<T extends EntityInterface> {
|
||||
fieldValue.textValue(), customPropertyType, propertyConfig, fieldName);
|
||||
jsonNode.put(fieldName, formattedValue);
|
||||
}
|
||||
case "enumWithDescriptions" -> handleEnumWithDescriptions(
|
||||
fieldName, fieldValue, propertyConfig, jsonNode, entity);
|
||||
case "table-cp" -> validateTableType(fieldValue, propertyConfig, fieldName);
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
@ -1532,37 +1530,54 @@ public abstract class EntityRepository<T extends EntityInterface> {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
private void validateTableType(JsonNode fieldValue, String propertyConfig, String fieldName) {
|
||||
TableConfig tableConfig =
|
||||
JsonUtils.convertValue(JsonUtils.readTree(propertyConfig), TableConfig.class);
|
||||
org.openmetadata.schema.type.customproperties.Table tableValue =
|
||||
JsonUtils.convertValue(
|
||||
JsonUtils.readTree(String.valueOf(fieldValue)),
|
||||
org.openmetadata.schema.type.customproperties.Table.class);
|
||||
Set<String> configColumns = tableConfig.getColumns();
|
||||
|
||||
try {
|
||||
JsonUtils.validateJsonSchema(
|
||||
tableValue, org.openmetadata.schema.type.customproperties.Table.class);
|
||||
|
||||
Set<String> fieldColumns = new HashSet<>();
|
||||
fieldValue.get("columns").forEach(column -> fieldColumns.add(column.asText()));
|
||||
|
||||
Set<String> undefinedColumns = new HashSet<>(fieldColumns);
|
||||
undefinedColumns.removeAll(configColumns);
|
||||
if (!undefinedColumns.isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Expected columns: "
|
||||
+ configColumns
|
||||
+ ", but found undefined columns: "
|
||||
+ undefinedColumns);
|
||||
}
|
||||
|
||||
if (fieldValue.get("rows").size() > tableConfig.getRowCount()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Number of rows should be less than or equal to the expected row count "
|
||||
+ tableConfig.getRowCount());
|
||||
}
|
||||
|
||||
Set<String> rowFieldNames = new HashSet<>();
|
||||
fieldValue.get("rows").forEach(row -> row.fieldNames().forEachRemaining(rowFieldNames::add));
|
||||
|
||||
undefinedColumns = new HashSet<>(rowFieldNames);
|
||||
undefinedColumns.removeAll(configColumns);
|
||||
if (!undefinedColumns.isEmpty()) {
|
||||
throw new IllegalArgumentException("Rows contain undefined columns: " + undefinedColumns);
|
||||
}
|
||||
} catch (ConstraintViolationException e) {
|
||||
String validationErrors =
|
||||
e.getConstraintViolations().stream()
|
||||
.map(violation -> violation.getPropertyPath() + " " + violation.getMessage())
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
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<String, JsonNode> 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));
|
||||
CatalogExceptionMessage.jsonValidationError(fieldName, validationErrors));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -22,6 +22,8 @@ import static org.openmetadata.service.Entity.FIELD_DESCRIPTION;
|
||||
import static org.openmetadata.service.util.EntityUtil.customFieldMatch;
|
||||
import static org.openmetadata.service.util.EntityUtil.getCustomField;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
@ -29,6 +31,7 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.validation.ConstraintViolationException;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.tuple.Triple;
|
||||
@ -41,10 +44,10 @@ 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.schema.type.customproperties.TableConfig;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.TypeRegistry;
|
||||
import org.openmetadata.service.exception.CatalogExceptionMessage;
|
||||
import org.openmetadata.service.resources.types.TypeResource;
|
||||
import org.openmetadata.service.util.EntityUtil;
|
||||
import org.openmetadata.service.util.EntityUtil.Fields;
|
||||
@ -173,8 +176,7 @@ public class TypeRepository extends EntityRepository<Type> {
|
||||
private void validateProperty(CustomProperty customProperty) {
|
||||
switch (customProperty.getPropertyType().getName()) {
|
||||
case "enum" -> validateEnumConfig(customProperty.getCustomPropertyConfig());
|
||||
case "enumWithDescriptions" -> validateEnumWithDescriptionsConfig(
|
||||
customProperty.getCustomPropertyConfig());
|
||||
case "table-cp" -> validateTableTypeConfig(customProperty.getCustomPropertyConfig());
|
||||
case "date-cp" -> validateDateFormat(
|
||||
customProperty.getCustomPropertyConfig(), getDateTokens(), "Invalid date format");
|
||||
case "dateTime-cp" -> validateDateFormat(
|
||||
@ -233,25 +235,37 @@ public class TypeRepository extends EntityRepository<Type> {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
private void validateTableTypeConfig(CustomPropertyConfig config) {
|
||||
if (config == null) {
|
||||
throw new IllegalArgumentException("Table Custom Property Type must have config populated.");
|
||||
}
|
||||
|
||||
JsonNode configNode = JsonUtils.valueToTree(config.getConfig());
|
||||
TableConfig tableConfig = JsonUtils.convertValue(config.getConfig(), TableConfig.class);
|
||||
|
||||
// rowCount is optional, if not present set it to the default value
|
||||
if (!configNode.has("rowCount")) {
|
||||
((ObjectNode) configNode).put("rowCount", tableConfig.getRowCount());
|
||||
config.setConfig(configNode);
|
||||
}
|
||||
|
||||
List<String> columns = new ArrayList<>();
|
||||
configNode.path("columns").forEach(node -> columns.add(node.asText()));
|
||||
Set<String> uniqueColumns = new HashSet<>(columns);
|
||||
if (uniqueColumns.size() != columns.size()) {
|
||||
throw new IllegalArgumentException("Column names must be unique.");
|
||||
}
|
||||
|
||||
try {
|
||||
JsonUtils.validateJsonSchema(config.getConfig(), TableConfig.class);
|
||||
} catch (ConstraintViolationException e) {
|
||||
String validationErrors =
|
||||
e.getConstraintViolations().stream()
|
||||
.map(violation -> violation.getPropertyPath() + " " + violation.getMessage())
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
"EnumWithDescriptions Custom Property Type must have customPropertyConfig.");
|
||||
CatalogExceptionMessage.customPropertyConfigError("table", validationErrors));
|
||||
}
|
||||
}
|
||||
|
||||
@ -413,27 +427,6 @@ public class TypeRepository extends EntityRepository<Type> {
|
||||
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<String> 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -398,8 +398,7 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
|
||||
public static Type TIMESTAMP_TYPE;
|
||||
|
||||
public static Type ENUM_TYPE;
|
||||
|
||||
public static Type ENUM_WITH_DESCRIPTIONS_TYPE;
|
||||
public static Type TABLE_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,
|
||||
|
||||
@ -643,49 +643,6 @@ public class GlossaryResourceTest extends EntityResourceTest<Glossary, CreateGlo
|
||||
11, "glossaryTermDateCp", DATECP_TYPE.getDisplayName(), "dd-MM-yyyy"))
|
||||
};
|
||||
assertRows(result, expectedRows);
|
||||
|
||||
// Create glossaryTerm with Invalid custom property of type enumWithDescriptions
|
||||
CustomProperty glossaryTermEnumCp =
|
||||
new CustomProperty()
|
||||
.withName("glossaryTermEnumWithDescriptionsCp")
|
||||
.withDescription("enumWithDescriptions type custom property with multiselect = true")
|
||||
.withPropertyType(ENUM_WITH_DESCRIPTIONS_TYPE.getEntityReference())
|
||||
.withCustomPropertyConfig(
|
||||
new CustomPropertyConfig()
|
||||
.withConfig(
|
||||
Map.of(
|
||||
"values",
|
||||
List.of(
|
||||
Map.of("key", "key1", "description", "description1"),
|
||||
Map.of("key", "key2", "description", "description2")),
|
||||
"multiSelect",
|
||||
true)));
|
||||
entityType =
|
||||
typeResourceTest.getEntityByName(
|
||||
Entity.GLOSSARY_TERM, "customProperties", ADMIN_AUTH_HEADERS);
|
||||
entityType =
|
||||
typeResourceTest.addAndCheckCustomProperty(
|
||||
entityType.getId(), glossaryTermEnumCp, OK, ADMIN_AUTH_HEADERS);
|
||||
String invalidEnumWithDescriptionRecord =
|
||||
",g1,dsp1,dsc1,h1;h2;h3,,term1;http://term1,PII.None,,,,glossaryTermEnumWithDescriptionsCp:key1|key3";
|
||||
String invalidEnumWithDescriptionValue =
|
||||
",g1,dsp1,dsc1,h1;h2;h3,,term1;http://term1,PII.None,,,,glossaryTermEnumWithDescriptionsCp:key1|key3";
|
||||
csv = createCsv(GlossaryCsv.HEADERS, listOf(invalidEnumWithDescriptionValue), null);
|
||||
result = importCsv(glossaryName, csv, false);
|
||||
Awaitility.await().atMost(4, TimeUnit.SECONDS).until(() -> true);
|
||||
assertSummary(result, ApiStatus.PARTIAL_SUCCESS, 2, 1, 1);
|
||||
expectedRows =
|
||||
new String[] {
|
||||
resultsHeader,
|
||||
getFailedRecord(
|
||||
invalidEnumWithDescriptionRecord,
|
||||
invalidCustomPropertyValue(
|
||||
11,
|
||||
"glossaryTermEnumWithDescriptionsCp",
|
||||
ENUM_WITH_DESCRIPTIONS_TYPE.getDisplayName(),
|
||||
"key3 not found in propertyConfig of glossaryTermEnumWithDescriptionsCp"))
|
||||
};
|
||||
assertRows(result, expectedRows);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -810,21 +767,7 @@ public class GlossaryResourceTest extends EntityResourceTest<Glossary, CreateGlo
|
||||
.withName("glossaryTermEnumCpMulti")
|
||||
.withDescription("enum type custom property with multiselect = true")
|
||||
.withPropertyType(ENUM_TYPE.getEntityReference())
|
||||
.withCustomPropertyConfig(enumConfig),
|
||||
new CustomProperty()
|
||||
.withName("glossaryTermEnumWithDescriptionsCp")
|
||||
.withDescription("enumWithDescriptions type custom property with multiselect = true")
|
||||
.withPropertyType(ENUM_WITH_DESCRIPTIONS_TYPE.getEntityReference())
|
||||
.withCustomPropertyConfig(
|
||||
new CustomPropertyConfig()
|
||||
.withConfig(
|
||||
Map.of(
|
||||
"values",
|
||||
List.of(
|
||||
Map.of("key", "key1", "description", "description1"),
|
||||
Map.of("key", "key2", "description", "description2")),
|
||||
"multiSelect",
|
||||
true)))
|
||||
.withCustomPropertyConfig(enumConfig)
|
||||
};
|
||||
|
||||
for (CustomProperty customProperty : customProperties) {
|
||||
@ -845,7 +788,7 @@ public class GlossaryResourceTest extends EntityResourceTest<Glossary, CreateGlo
|
||||
",g2,dsp2,dsc3,h1;h3;h3,,term2;https://term2,PII.NonSensitive,,user:%s,%s,\"glossaryTermEnumCpMulti:val3|val2|val1|val4|val5;glossaryTermEnumCpSingle:singleVal1;glossaryTermIntegerCp:7777;glossaryTermMarkdownCp:# Sample Markdown Text;glossaryTermNumberCp:123456;\"\"glossaryTermQueryCp:select col,row from table where id ='30';\"\";glossaryTermStringCp:sample string content;glossaryTermTimeCp:10:08:45;glossaryTermTimeIntervalCp:1726142300000:17261420000;glossaryTermTimestampCp:1726142400000\"",
|
||||
user1, "Approved"),
|
||||
String.format(
|
||||
"importExportTest.g1,g11,dsp2,dsc11,h1;h3;h3,,,,user:%s,team:%s,%s,glossaryTermEnumWithDescriptionsCp:key1|key2",
|
||||
"importExportTest.g1,g11,dsp2,dsc11,h1;h3;h3,,,,user:%s,team:%s,%s,",
|
||||
reviewerRef.get(0), team11, "Draft"));
|
||||
|
||||
// Update terms with change in description
|
||||
@ -858,7 +801,7 @@ public class GlossaryResourceTest extends EntityResourceTest<Glossary, CreateGlo
|
||||
",g2,dsp2,new-dsc3,h1;h3;h3,,term2;https://term2,PII.NonSensitive,user:%s,user:%s,%s,\"glossaryTermEnumCpMulti:val3|val2|val1|val4|val5;glossaryTermEnumCpSingle:singleVal1;glossaryTermIntegerCp:7777;glossaryTermMarkdownCp:# Sample Markdown Text;glossaryTermNumberCp:123456;\"\"glossaryTermQueryCp:select col,row from table where id ='30';\"\";glossaryTermStringCp:sample string content;glossaryTermTimeCp:10:08:45;glossaryTermTimeIntervalCp:1726142300000:17261420000;glossaryTermTimestampCp:1726142400000\"",
|
||||
user1, user2, "Approved"),
|
||||
String.format(
|
||||
"importExportTest.g1,g11,dsp2,new-dsc11,h1;h3;h3,,,,user:%s,team:%s,%s,glossaryTermEnumWithDescriptionsCp:key1|key2",
|
||||
"importExportTest.g1,g11,dsp2,new-dsc11,h1;h3;h3,,,,user:%s,team:%s,%s,",
|
||||
reviewerRef.get(0), team11, "Draft"));
|
||||
|
||||
// Add new row to existing rows
|
||||
|
||||
@ -25,10 +25,14 @@ import static org.openmetadata.service.util.TestUtils.assertCustomProperties;
|
||||
import static org.openmetadata.service.util.TestUtils.assertResponse;
|
||||
import static org.openmetadata.service.util.TestUtils.assertResponseContains;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import javax.ws.rs.client.WebTarget;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
@ -45,8 +49,7 @@ 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.schema.type.customproperties.EnumWithDescriptionsConfig;
|
||||
import org.openmetadata.schema.type.customproperties.Value;
|
||||
import org.openmetadata.schema.type.customproperties.TableConfig;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.resources.EntityResourceTest;
|
||||
import org.openmetadata.service.resources.types.TypeResource;
|
||||
@ -73,7 +76,6 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
|
||||
STRING_TYPE = getEntityByName("string", "", ADMIN_AUTH_HEADERS);
|
||||
EMAIL_TYPE = getEntityByName("email", "", ADMIN_AUTH_HEADERS);
|
||||
ENUM_TYPE = getEntityByName("enum", "", ADMIN_AUTH_HEADERS);
|
||||
ENUM_WITH_DESCRIPTIONS_TYPE = getEntityByName("enumWithDescriptions", "", ADMIN_AUTH_HEADERS);
|
||||
DATECP_TYPE = getEntityByName("date-cp", "", ADMIN_AUTH_HEADERS);
|
||||
DATETIMECP_TYPE = getEntityByName("dateTime-cp", "", ADMIN_AUTH_HEADERS);
|
||||
TIMECP_TYPE = getEntityByName("time-cp", "", ADMIN_AUTH_HEADERS);
|
||||
@ -85,6 +87,7 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
|
||||
NUMBER_TYPE = getEntityByName("number", "", ADMIN_AUTH_HEADERS);
|
||||
SQLQUERY_TYPE = getEntityByName("sqlQuery", "", ADMIN_AUTH_HEADERS);
|
||||
TIMESTAMP_TYPE = getEntityByName("timestamp", "", ADMIN_AUTH_HEADERS);
|
||||
TABLE_TYPE = getEntityByName("table-cp", "", ADMIN_AUTH_HEADERS);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -296,174 +299,146 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
|
||||
}
|
||||
|
||||
@Test
|
||||
void put_patch_customProperty_enumWithDescriptions_200() throws IOException {
|
||||
void put_patch_customProperty_table_200() throws IOException {
|
||||
Type databaseEntity = getEntityByName("database", "customProperties", ADMIN_AUTH_HEADERS);
|
||||
TableConfig tableConfig = new TableConfig();
|
||||
|
||||
// Add a custom property of type enumWithDescriptions with PUT
|
||||
CustomProperty enumWithDescriptionsFieldA =
|
||||
// Add a custom property of type table with PUT
|
||||
CustomProperty tableTypeFieldA =
|
||||
new CustomProperty()
|
||||
.withName("enumWithDescriptionsTest")
|
||||
.withDescription("enumWithDescriptionsTest")
|
||||
.withPropertyType(ENUM_WITH_DESCRIPTIONS_TYPE.getEntityReference());
|
||||
.withName("tableCustomPropertyTest")
|
||||
.withDescription("tableCustomPropertyTest description")
|
||||
.withPropertyType(TABLE_TYPE.getEntityReference());
|
||||
ChangeDescription change = getChangeDescription(databaseEntity, MINOR_UPDATE);
|
||||
fieldAdded(change, "customProperties", new ArrayList<>(List.of(enumWithDescriptionsFieldA)));
|
||||
fieldAdded(change, "customProperties", new ArrayList<>(List.of(tableTypeFieldA)));
|
||||
Type finalDatabaseEntity = databaseEntity;
|
||||
ChangeDescription finalChange = change;
|
||||
assertResponseContains(
|
||||
() ->
|
||||
addCustomPropertyAndCheck(
|
||||
finalDatabaseEntity.getId(),
|
||||
enumWithDescriptionsFieldA,
|
||||
tableTypeFieldA,
|
||||
ADMIN_AUTH_HEADERS,
|
||||
MINOR_UPDATE,
|
||||
finalChange),
|
||||
Status.BAD_REQUEST,
|
||||
"EnumWithDescriptions Custom Property Type must have customPropertyConfig.");
|
||||
enumWithDescriptionsFieldA.setCustomPropertyConfig(
|
||||
new CustomPropertyConfig().withConfig(new EnumWithDescriptionsConfig()));
|
||||
"Table Custom Property Type must have config populated.");
|
||||
|
||||
tableTypeFieldA.setCustomPropertyConfig(
|
||||
new CustomPropertyConfig().withConfig(new TableConfig()));
|
||||
ChangeDescription change1 = getChangeDescription(databaseEntity, MINOR_UPDATE);
|
||||
Type databaseEntity1 = databaseEntity;
|
||||
assertResponseContains(
|
||||
() ->
|
||||
addCustomPropertyAndCheck(
|
||||
databaseEntity1.getId(),
|
||||
enumWithDescriptionsFieldA,
|
||||
tableTypeFieldA,
|
||||
ADMIN_AUTH_HEADERS,
|
||||
MINOR_UPDATE,
|
||||
change1),
|
||||
Status.BAD_REQUEST,
|
||||
"EnumWithDescriptions Custom Property Type must have customPropertyConfig populated with values.");
|
||||
"Custom Property table has invalid value columns size must be between "
|
||||
+ tableConfig.getMinColumns()
|
||||
+ " and "
|
||||
+ tableConfig.getMaxColumns());
|
||||
|
||||
List<Value> 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(
|
||||
tableTypeFieldA.setCustomPropertyConfig(
|
||||
new CustomPropertyConfig()
|
||||
.withConfig(new EnumWithDescriptionsConfig().withValues(valuesWithDuplicateKey)));
|
||||
ChangeDescription change7 = getChangeDescription(databaseEntity, MINOR_UPDATE);
|
||||
.withConfig(
|
||||
new TableConfig()
|
||||
.withColumns(Set.of("column-1", "column-2", "column-3"))
|
||||
.withRowCount(200)));
|
||||
ChangeDescription change2 = getChangeDescription(databaseEntity, MINOR_UPDATE);
|
||||
Type databaseEntity2 = databaseEntity;
|
||||
assertResponseContains(
|
||||
() ->
|
||||
addCustomPropertyAndCheck(
|
||||
databaseEntity2.getId(),
|
||||
enumWithDescriptionsFieldA,
|
||||
tableTypeFieldA,
|
||||
ADMIN_AUTH_HEADERS,
|
||||
MINOR_UPDATE,
|
||||
change7),
|
||||
change2),
|
||||
Status.BAD_REQUEST,
|
||||
"EnumWithDescriptions Custom Property key cannot have duplicates.");
|
||||
List<Value> 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());
|
||||
"Custom Property table has invalid value rowCount must be less than or equal to "
|
||||
+ tableConfig.getMaxRows());
|
||||
|
||||
List<Value> 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)));
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ObjectNode tableConfigJson = mapper.createObjectNode();
|
||||
ArrayNode columnsArray = tableConfigJson.putArray("columns");
|
||||
columnsArray.add("col 1");
|
||||
columnsArray.add("col 2");
|
||||
columnsArray.add("col");
|
||||
columnsArray.add("col");
|
||||
|
||||
tableTypeFieldA.setCustomPropertyConfig(new CustomPropertyConfig().withConfig(tableConfigJson));
|
||||
ChangeDescription change3 = getChangeDescription(databaseEntity, MINOR_UPDATE);
|
||||
Type databaseEntity3 = databaseEntity;
|
||||
assertResponseContains(
|
||||
() ->
|
||||
addCustomPropertyAndCheck(
|
||||
databaseEntity1.getId(),
|
||||
enumWithDescriptionsFieldA,
|
||||
databaseEntity3.getId(),
|
||||
tableTypeFieldA,
|
||||
ADMIN_AUTH_HEADERS,
|
||||
MINOR_UPDATE,
|
||||
change3),
|
||||
Status.BAD_REQUEST,
|
||||
"Existing EnumWithDescriptions Custom Property values cannot be removed.");
|
||||
"Column names must be unique.");
|
||||
|
||||
enumWithDescriptionsFieldA.setCustomPropertyConfig(
|
||||
tableTypeFieldA.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());
|
||||
.withConfig(new TableConfig().withColumns(Set.of("column1", "column2", "column3"))));
|
||||
databaseEntity =
|
||||
addCustomPropertyAndCheck(
|
||||
databaseEntity.getId(),
|
||||
enumWithDescriptionsFieldA,
|
||||
ADMIN_AUTH_HEADERS,
|
||||
MINOR_UPDATE,
|
||||
change5);
|
||||
databaseEntity.getId(), tableTypeFieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change);
|
||||
assertCustomProperties(
|
||||
new ArrayList<>(List.of(enumWithDescriptionsFieldA)), databaseEntity.getCustomProperties());
|
||||
new ArrayList<>(List.of(tableTypeFieldA)), databaseEntity.getCustomProperties());
|
||||
|
||||
CustomPropertyConfig prevConfig = tableTypeFieldA.getCustomPropertyConfig();
|
||||
|
||||
// Changing custom property description with PUT
|
||||
tableTypeFieldA.withDescription("updated tableCustomPropertyTest description");
|
||||
ChangeDescription change5 = getChangeDescription(databaseEntity, MINOR_UPDATE);
|
||||
fieldUpdated(
|
||||
change5,
|
||||
EntityUtil.getCustomField(tableTypeFieldA, "description"),
|
||||
"tableCustomPropertyTest description",
|
||||
"updated tableCustomPropertyTest description");
|
||||
databaseEntity =
|
||||
addCustomPropertyAndCheck(
|
||||
databaseEntity.getId(), tableTypeFieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change5);
|
||||
assertCustomProperties(
|
||||
new ArrayList<>(List.of(tableTypeFieldA)), databaseEntity.getCustomProperties());
|
||||
|
||||
ChangeDescription change6 = getChangeDescription(databaseEntity, MINOR_UPDATE);
|
||||
tableTypeFieldA.setCustomPropertyConfig(
|
||||
new CustomPropertyConfig()
|
||||
.withConfig(new TableConfig().withColumns(Set.of("column-1", "column-2", "column-3"))));
|
||||
fieldUpdated(
|
||||
change6,
|
||||
EntityUtil.getCustomField(tableTypeFieldA, "customPropertyConfig"),
|
||||
prevConfig,
|
||||
tableTypeFieldA.getCustomPropertyConfig());
|
||||
databaseEntity =
|
||||
addCustomPropertyAndCheck(
|
||||
databaseEntity.getId(), tableTypeFieldA, ADMIN_AUTH_HEADERS, MINOR_UPDATE, change6);
|
||||
assertCustomProperties(
|
||||
new ArrayList<>(List.of(tableTypeFieldA)), databaseEntity.getCustomProperties());
|
||||
|
||||
// Changing custom property description with PATCH
|
||||
// Changes from this PATCH is consolidated with the previous changes
|
||||
enumWithDescriptionsFieldA.withDescription("updated2");
|
||||
tableTypeFieldA.withDescription("updated tableCustomPropertyTest description 2");
|
||||
String json = JsonUtils.pojoToJson(databaseEntity);
|
||||
databaseEntity.setCustomProperties(List.of(enumWithDescriptionsFieldA));
|
||||
databaseEntity.setCustomProperties(List.of(tableTypeFieldA));
|
||||
change = getChangeDescription(databaseEntity, CHANGE_CONSOLIDATED);
|
||||
|
||||
fieldUpdated(
|
||||
change5,
|
||||
EntityUtil.getCustomField(enumWithDescriptionsFieldA, "description"),
|
||||
"updatedEnumWithDescriptionsTest",
|
||||
"updated2");
|
||||
change6,
|
||||
EntityUtil.getCustomField(tableTypeFieldA, "description"),
|
||||
"updated tableCustomPropertyTest description",
|
||||
"updated tableCustomPropertyTest description 2");
|
||||
|
||||
databaseEntity =
|
||||
patchEntityAndCheck(databaseEntity, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change5);
|
||||
patchEntityAndCheck(databaseEntity, json, ADMIN_AUTH_HEADERS, CHANGE_CONSOLIDATED, change6);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
{
|
||||
"$id": "https://open-metadata.org/schema/type/customProperties/complexTypes.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Basic",
|
||||
"description": "This schema defines custom properties complex types.",
|
||||
"definitions": {
|
||||
"entityReference": {
|
||||
@ -99,6 +98,47 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"table-cp": {
|
||||
"$comment": "@om-field-type",
|
||||
"title": "Table",
|
||||
"description": "A table-type custom property having rows and columns where all column data types are strings.",
|
||||
"type": "object",
|
||||
"javaType": "org.openmetadata.schema.type.customproperties.Table",
|
||||
"properties": {
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"description": "List of column names defined at the entity type level.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "The name of the column."
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 3,
|
||||
"uniqueItems": true
|
||||
},
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"description": "List of rows added at the entity instance level. Each row contains dynamic fields based on the defined columns.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "A row in the table, with dynamic key-value pairs corresponding to columns.",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"description": "The cell value of each column in the row."
|
||||
}
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 10
|
||||
}
|
||||
},
|
||||
"required": ["columns"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"table-cp": {
|
||||
"$ref": "#/definitions/table-cp"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
{
|
||||
"$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
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
{
|
||||
"$id": "https://open-metadata.org/schema/type/customProperties/tableConfig.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "TableConfig",
|
||||
"description": "Custom property configuration for table-type property where all column data types are strings.",
|
||||
"type": "object",
|
||||
"javaType": "org.openmetadata.schema.type.customproperties.TableConfig",
|
||||
"properties": {
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"description": "List of column names defined at the entity type level.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "The name of the column."
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 3,
|
||||
"uniqueItems": true
|
||||
},
|
||||
"rowCount": {
|
||||
"type": "integer",
|
||||
"default": 10,
|
||||
"description": "Number of rows. Defaults to maxRows if not explicitly set.",
|
||||
"minimum": 1,
|
||||
"maximum": 10
|
||||
},
|
||||
"minColumns": {
|
||||
"type": "integer",
|
||||
"default": 1,
|
||||
"$comment": "For internal use only: Minimum number of columns."
|
||||
},
|
||||
"maxColumns": {
|
||||
"type": "integer",
|
||||
"default": 3,
|
||||
"$comment": "For internal use only: Maximum number of columns."
|
||||
},
|
||||
"minRows": {
|
||||
"type": "integer",
|
||||
"default": 1,
|
||||
"$comment": "For internal use only: Minimum number of rows."
|
||||
},
|
||||
"maxRows": {
|
||||
"type": "integer",
|
||||
"default": 10,
|
||||
"$comment": "For internal use only: Maximum number of rows."
|
||||
}
|
||||
},
|
||||
"required": ["columns"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@ -35,7 +35,7 @@
|
||||
"$ref": "#/definitions/entityTypes"
|
||||
},
|
||||
{
|
||||
"$ref": "../type/customProperties/enumWithDescriptionsConfig.json"
|
||||
"$ref": "customProperties/tableConfig.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ export const CUSTOM_PROPERTIES_TYPES = {
|
||||
STRING: 'String',
|
||||
MARKDOWN: 'Markdown',
|
||||
SQL_QUERY: 'Sql Query',
|
||||
ENUM_WITH_DESCRIPTIONS: 'Enum With Descriptions',
|
||||
};
|
||||
|
||||
export const FIELD_VALUES_CUSTOM_PROPERTIES = {
|
||||
|
||||
@ -52,39 +52,6 @@ test.describe('Custom properties with custom property config', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
() => {
|
||||
|
||||
@ -17,8 +17,8 @@ import {
|
||||
ENTITY_REFERENCE_PROPERTIES,
|
||||
} from '../constant/customProperty';
|
||||
import {
|
||||
EntityTypeEndpoint,
|
||||
ENTITY_PATH,
|
||||
EntityTypeEndpoint,
|
||||
} from '../support/entity/Entity.interface';
|
||||
import { UserClass } from '../support/user/UserClass';
|
||||
import { clickOutside, descriptionBox, uuid } from './common';
|
||||
@ -44,7 +44,6 @@ export enum CustomPropertyTypeByName {
|
||||
TIME_CP = 'time-cp',
|
||||
DATE_CP = 'date-cp',
|
||||
DATE_TIME_CP = 'dateTime-cp',
|
||||
ENUM_WITH_DESCRIPTION = 'enumWithDescriptions',
|
||||
}
|
||||
|
||||
export interface CustomProperty {
|
||||
@ -123,15 +122,6 @@ export const setValueForProperty = async (data: {
|
||||
|
||||
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);
|
||||
@ -251,18 +241,6 @@ export const validateValueForProperty = async (data: {
|
||||
);
|
||||
} else if (propertyType === 'sqlQuery') {
|
||||
await expect(container.locator('.CodeMirror-scroll')).toContainText(value);
|
||||
} else if (propertyType === 'enumWithDescriptions') {
|
||||
await expect(
|
||||
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',
|
||||
@ -316,11 +294,6 @@ export const getPropertyValues = (
|
||||
value: 'small',
|
||||
newValue: 'medium',
|
||||
};
|
||||
case 'enumWithDescriptions':
|
||||
return {
|
||||
value: 'enumWithDescription1',
|
||||
newValue: 'enumWithDescription2',
|
||||
};
|
||||
case 'sqlQuery':
|
||||
return {
|
||||
value: 'Select * from table',
|
||||
@ -447,25 +420,6 @@ 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: {
|
||||
|
||||
@ -11,7 +11,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { expect, Page } from '@playwright/test';
|
||||
import { CUSTOM_PROPERTIES_ENTITIES } from '../constant/customProperty';
|
||||
import {
|
||||
CUSTOM_PROPERTIES_TYPES,
|
||||
FIELD_VALUES_CUSTOM_PROPERTIES,
|
||||
@ -174,32 +173,6 @@ const editGlossaryCustomProperty = async (
|
||||
|
||||
await page.getByTestId('inline-save-btn').click();
|
||||
}
|
||||
|
||||
if (type === CUSTOM_PROPERTIES_TYPES.ENUM_WITH_DESCRIPTIONS) {
|
||||
await page.getByTestId('enum-with-description-select').click();
|
||||
|
||||
await page.waitForSelector('.ant-select-dropdown', {
|
||||
state: 'visible',
|
||||
});
|
||||
|
||||
// await page
|
||||
// .getByRole('option', {
|
||||
// name: CUSTOM_PROPERTIES_ENTITIES.entity_glossaryTerm
|
||||
// .enumWithDescriptionConfig.values[0].key,
|
||||
// })
|
||||
// .click();
|
||||
|
||||
await page
|
||||
.locator('span')
|
||||
.filter({
|
||||
hasText:
|
||||
CUSTOM_PROPERTIES_ENTITIES.entity_glossaryTerm
|
||||
.enumWithDescriptionConfig.values[0].key,
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.getByTestId('inline-save-btn').click();
|
||||
}
|
||||
};
|
||||
|
||||
export const fillCustomPropertyDetails = async (
|
||||
|
||||
@ -11,14 +11,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Button, Modal, Typography } from 'antd';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { AxiosError } from 'axios';
|
||||
import { isObject } from 'lodash';
|
||||
import { EntityType } from '../../../enums/entity.enum';
|
||||
import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm';
|
||||
import { EnumConfig, Type, ValueClass } from '../../../generated/entity/type';
|
||||
import { Type } from '../../../generated/entity/type';
|
||||
import { getTypeByFQN } from '../../../rest/metadataTypeAPI';
|
||||
import {
|
||||
convertCustomPropertyStringToEntityExtension,
|
||||
@ -47,20 +46,6 @@ export const ModalWithCustomPropertyEditor = ({
|
||||
useState<ExtensionDataProps>();
|
||||
const [customPropertyTypes, setCustomPropertyTypes] = useState<Type>();
|
||||
|
||||
const enumWithDescriptionsKeyPairValues = useMemo(() => {
|
||||
const valuesWithEnumKey: Record<string, ValueClass[]> = {};
|
||||
|
||||
customPropertyTypes?.customProperties?.forEach((property) => {
|
||||
if (property.propertyType.name === 'enumWithDescriptions') {
|
||||
valuesWithEnumKey[property.name] = (
|
||||
property.customPropertyConfig?.config as EnumConfig
|
||||
).values as ValueClass[];
|
||||
}
|
||||
});
|
||||
|
||||
return valuesWithEnumKey;
|
||||
}, [customPropertyTypes]);
|
||||
|
||||
const fetchTypeDetail = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@ -87,42 +72,8 @@ export const ModalWithCustomPropertyEditor = ({
|
||||
setIsSaveLoading(false);
|
||||
};
|
||||
|
||||
// EnumWithDescriptions values are change only contain keys,
|
||||
// so we need to modify the extension data to include descriptions for them to display in the table
|
||||
const modifyExtensionData = useCallback(
|
||||
(extension: ExtensionDataProps) => {
|
||||
const modifiedExtension = Object.entries(extension).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (enumWithDescriptionsKeyPairValues[key]) {
|
||||
return {
|
||||
...acc,
|
||||
[key]: (value as string[] | ValueClass[]).map((item) => {
|
||||
if (isObject(item)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return {
|
||||
key: item,
|
||||
description: enumWithDescriptionsKeyPairValues[key].find(
|
||||
(val) => val.key === item
|
||||
)?.description,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return { ...acc, [key]: value };
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return modifiedExtension;
|
||||
},
|
||||
[enumWithDescriptionsKeyPairValues]
|
||||
);
|
||||
|
||||
const onExtensionUpdate = async (data: GlossaryTerm) => {
|
||||
setCustomPropertyValue(modifyExtensionData(data.extension));
|
||||
setCustomPropertyValue(data.extension);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -11,11 +11,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Col, Form, Input, Row } from 'antd';
|
||||
import { Button, Col, Form, Row } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { t } from 'i18next';
|
||||
import { isUndefined, map, omit, omitBy, startCase } from 'lodash';
|
||||
import { isArray, isUndefined, map, omit, omitBy, startCase } from 'lodash';
|
||||
import React, {
|
||||
FocusEvent,
|
||||
useCallback,
|
||||
@ -24,13 +23,12 @@ 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,
|
||||
TABLE_TYPE_CUSTOM_PROPERTY,
|
||||
} from '../../../../constants/CustomProperty.constants';
|
||||
import { GlobalSettingsMenuCategory } from '../../../../constants/GlobalSettings.constants';
|
||||
import { CUSTOM_PROPERTY_NAME_REGEX } from '../../../../constants/regex.constants';
|
||||
@ -41,7 +39,6 @@ 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,
|
||||
@ -58,7 +55,6 @@ 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';
|
||||
|
||||
@ -112,7 +108,7 @@ const AddCustomProperty = () => {
|
||||
hasFormatConfig,
|
||||
hasEntityReferenceConfig,
|
||||
watchedOption,
|
||||
hasEnumWithDescriptionConfig,
|
||||
hasTableTypeConfig,
|
||||
} = useMemo(() => {
|
||||
const watchedOption = propertyTypeOptions.find(
|
||||
(option) => option.value === watchedPropertyType
|
||||
@ -121,8 +117,7 @@ const AddCustomProperty = () => {
|
||||
|
||||
const hasEnumConfig = watchedOptionKey === 'enum';
|
||||
|
||||
const hasEnumWithDescriptionConfig =
|
||||
watchedOptionKey === ENUM_WITH_DESCRIPTION;
|
||||
const hasTableTypeConfig = watchedOptionKey === TABLE_TYPE_CUSTOM_PROPERTY;
|
||||
|
||||
const hasFormatConfig =
|
||||
PROPERTY_TYPES_WITH_FORMAT.includes(watchedOptionKey);
|
||||
@ -135,7 +130,7 @@ const AddCustomProperty = () => {
|
||||
hasFormatConfig,
|
||||
hasEntityReferenceConfig,
|
||||
watchedOption,
|
||||
hasEnumWithDescriptionConfig,
|
||||
hasTableTypeConfig,
|
||||
};
|
||||
}, [watchedPropertyType, propertyTypeOptions]);
|
||||
|
||||
@ -176,7 +171,8 @@ const AddCustomProperty = () => {
|
||||
formatConfig: string;
|
||||
entityReferenceConfig: string[];
|
||||
multiSelect?: boolean;
|
||||
enumWithDescriptionsConfig?: EnumWithDescriptionsConfig['values'];
|
||||
rowCount: number;
|
||||
columns: string[];
|
||||
}
|
||||
) => {
|
||||
if (isUndefined(typeDetail)) {
|
||||
@ -208,11 +204,11 @@ const AddCustomProperty = () => {
|
||||
};
|
||||
}
|
||||
|
||||
if (hasEnumWithDescriptionConfig) {
|
||||
if (hasTableTypeConfig) {
|
||||
customPropertyConfig = {
|
||||
config: {
|
||||
multiSelect: Boolean(data?.multiSelect),
|
||||
values: data.enumWithDescriptionsConfig,
|
||||
columns: data.columns,
|
||||
rowCount: data.rowCount ?? 10,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -224,7 +220,8 @@ const AddCustomProperty = () => {
|
||||
'formatConfig',
|
||||
'entityReferenceConfig',
|
||||
'enumConfig',
|
||||
'enumWithDescriptionsConfig',
|
||||
'rowCount',
|
||||
'columns',
|
||||
]),
|
||||
propertyType: {
|
||||
id: data.propertyType,
|
||||
@ -388,6 +385,71 @@ const AddCustomProperty = () => {
|
||||
},
|
||||
};
|
||||
|
||||
const tableTypePropertyConfig: FieldProp[] = [
|
||||
{
|
||||
name: 'columns',
|
||||
required: true,
|
||||
label: t('label.column-plural'),
|
||||
id: 'root/columns',
|
||||
type: FieldTypes.SELECT,
|
||||
props: {
|
||||
'data-testid': 'columns',
|
||||
mode: 'tags',
|
||||
placeholder: t('label.column-plural'),
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
validator: async (_, value) => {
|
||||
if (isArray(value)) {
|
||||
if (value.length > 3) {
|
||||
return Promise.reject(
|
||||
t('message.maximum-count-allowed', {
|
||||
count: 3,
|
||||
label: t('label.column-plural'),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject(
|
||||
t('label.field-required', {
|
||||
field: t('label.column-plural'),
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'rowCount',
|
||||
label: t('label.row-count'),
|
||||
type: FieldTypes.NUMBER,
|
||||
required: false,
|
||||
id: 'root/rowCount',
|
||||
props: {
|
||||
'data-testid': 'rowCount',
|
||||
size: 'default',
|
||||
style: { width: '100%' },
|
||||
placeholder: t('label.row-count'),
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
min: 1,
|
||||
type: 'number',
|
||||
max: 10,
|
||||
message: t('message.entity-size-in-between', {
|
||||
entity: t('label.row-count'),
|
||||
min: 1,
|
||||
max: 10,
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const firstPanelChildren = (
|
||||
<div className="max-width-md w-9/10 service-form-container">
|
||||
<TitleBreadcrumb titleLinks={slashedBreadcrumb} />
|
||||
@ -395,6 +457,9 @@ const AddCustomProperty = () => {
|
||||
className="m-t-md"
|
||||
data-testid="custom-property-form"
|
||||
form={form}
|
||||
initialValues={{
|
||||
rowCount: 10,
|
||||
}}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
onFocus={handleFieldFocus}>
|
||||
@ -415,94 +480,8 @@ const AddCustomProperty = () => {
|
||||
generateFormFields([entityReferenceConfigField])
|
||||
}
|
||||
|
||||
{hasEnumWithDescriptionConfig && (
|
||||
<>
|
||||
<Form.List name="enumWithDescriptionsConfig">
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<Form.Item
|
||||
className="form-item-horizontal"
|
||||
colon={false}
|
||||
label={t('label.property')}>
|
||||
<Button
|
||||
data-testid="add-enum-description-config"
|
||||
icon={
|
||||
<PlusOutlined
|
||||
style={{ color: 'white', fontSize: '12px' }}
|
||||
/>
|
||||
}
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
add();
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
{hasTableTypeConfig && generateFormFields(tableTypePropertyConfig)}
|
||||
|
||||
{fields.map((field, index) => (
|
||||
<Row gutter={[8, 0]} key={field.key}>
|
||||
<Col span={23}>
|
||||
<Row gutter={[8, 0]}>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name={[field.name, 'key']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `${t(
|
||||
'message.field-text-is-required',
|
||||
{
|
||||
fieldText: t('label.key'),
|
||||
}
|
||||
)}`,
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
id={`key-${index}`}
|
||||
placeholder={t('label.key')}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name={[field.name, 'description']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `${t(
|
||||
'message.field-text-is-required',
|
||||
{
|
||||
fieldText: t('label.description'),
|
||||
}
|
||||
)}`,
|
||||
},
|
||||
]}
|
||||
trigger="onTextChange"
|
||||
valuePropName="initialValue">
|
||||
<RichTextEditor height="200px" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={1}>
|
||||
<Button
|
||||
data-testid={`remove-enum-description-config-${index}`}
|
||||
icon={<DeleteIcon width={16} />}
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
{generateFormFields([multiSelectField])}
|
||||
</>
|
||||
)}
|
||||
{generateFormFields([descriptionField])}
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
|
||||
@ -10,19 +10,17 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Button, Space, Tag, Tooltip, Typography } from 'antd';
|
||||
import { Button, Space, Tooltip, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { isArray, 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';
|
||||
import { ReactComponent as IconDelete } from '../../../assets/svg/ic-delete.svg';
|
||||
import { ENUM_WITH_DESCRIPTION } from '../../../constants/CustomProperty.constants';
|
||||
import { ADD_CUSTOM_PROPERTIES_DOCS } from '../../../constants/docs.constants';
|
||||
import { NO_PERMISSION_FOR_ACTION } from '../../../constants/HelperTextUtil';
|
||||
import { TABLE_SCROLL_VALUE } from '../../../constants/Table.constants';
|
||||
import { ERROR_PLACEHOLDER_TYPE, OPERATION } from '../../../enums/common.enum';
|
||||
import { EnumWithDescriptionsConfig } from '../../../generated/type/customProperties/enumWithDescriptionsConfig';
|
||||
import { CustomProperty } from '../../../generated/type/customProperty';
|
||||
import { columnSorter, getEntityName } from '../../../utils/EntityUtils';
|
||||
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||
@ -70,9 +68,7 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
|
||||
const updatedProperties = customProperties.map((property) => {
|
||||
if (property.name === selectedProperty.name) {
|
||||
const config = data.customPropertyConfig;
|
||||
const isEnumType =
|
||||
selectedProperty.propertyType.name === 'enum' ||
|
||||
selectedProperty.propertyType.name === ENUM_WITH_DESCRIPTION;
|
||||
const isEnumType = selectedProperty.propertyType.name === 'enum';
|
||||
|
||||
return {
|
||||
...property,
|
||||
@ -144,36 +140,24 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
|
||||
|
||||
// If config is an object, then it is a enum config
|
||||
if (!isString(config) && !isArray(config)) {
|
||||
if (record.propertyType.name === ENUM_WITH_DESCRIPTION) {
|
||||
const values =
|
||||
(config?.values as EnumWithDescriptionsConfig['values']) ?? [];
|
||||
|
||||
if (config?.columns) {
|
||||
return (
|
||||
<div
|
||||
className="w-full d-flex gap-2 flex-column"
|
||||
data-testid="enum-with-description-config">
|
||||
<div className="w-full d-flex gap-2 flex-column">
|
||||
{values.map((value) => (
|
||||
<Tooltip
|
||||
key={value.key}
|
||||
title={value.description}
|
||||
trigger="hover">
|
||||
<Tag
|
||||
style={{
|
||||
width: 'max-content',
|
||||
margin: '0px',
|
||||
border: 'none',
|
||||
padding: '4px',
|
||||
background: 'rgba(0, 0, 0, 0.03)',
|
||||
}}>
|
||||
{value.key}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-full d-flex gap-2 flex-column">
|
||||
<Typography.Text>
|
||||
{t('label.multi-select')}:{' '}
|
||||
{config?.multiSelect ? t('label.yes') : t('label.no')}
|
||||
<span className="font-medium">{`${t(
|
||||
'label.column-plural'
|
||||
)}:`}</span>
|
||||
<ul className="m-b-0">
|
||||
{config.columns.map((column) => (
|
||||
<li key={column}>{column}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
<span className="font-medium">{`${t(
|
||||
'label.row-count'
|
||||
)}: `}</span>
|
||||
{config?.rowCount ?? 10}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -10,30 +10,17 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { InfoCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { get, isUndefined, uniq } from 'lodash';
|
||||
import { Form, Modal, Typography } from 'antd';
|
||||
import { isUndefined, uniq } from 'lodash';
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as DeleteIcon } from '../../../../assets/svg/ic-delete.svg';
|
||||
import {
|
||||
ENTITY_REFERENCE_OPTIONS,
|
||||
ENUM_WITH_DESCRIPTION,
|
||||
PROPERTY_TYPES_WITH_ENTITY_REFERENCE,
|
||||
} from '../../../../constants/CustomProperty.constants';
|
||||
import {
|
||||
Config,
|
||||
CustomProperty,
|
||||
EnumConfig,
|
||||
ValueClass,
|
||||
} from '../../../../generated/type/customProperty';
|
||||
import {
|
||||
FieldProp,
|
||||
@ -41,11 +28,10 @@ import {
|
||||
FormItemLayout,
|
||||
} from '../../../../interface/FormUtils.interface';
|
||||
import { generateFormFields } from '../../../../utils/formUtils';
|
||||
import RichTextEditor from '../../../common/RichTextEditor/RichTextEditor';
|
||||
|
||||
export interface FormData {
|
||||
description: string;
|
||||
customPropertyConfig: string[] | ValueClass[];
|
||||
customPropertyConfig: string[];
|
||||
multiSelect?: boolean;
|
||||
}
|
||||
|
||||
@ -72,21 +58,15 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const {
|
||||
hasEnumConfig,
|
||||
hasEntityReferenceConfig,
|
||||
hasEnumWithDescriptionConfig,
|
||||
} = useMemo(() => {
|
||||
const { hasEnumConfig, hasEntityReferenceConfig } = useMemo(() => {
|
||||
const propertyName = customProperty.propertyType.name ?? '';
|
||||
const hasEnumConfig = propertyName === 'enum';
|
||||
const hasEnumWithDescriptionConfig = propertyName === ENUM_WITH_DESCRIPTION;
|
||||
const hasEntityReferenceConfig =
|
||||
PROPERTY_TYPES_WITH_ENTITY_REFERENCE.includes(propertyName);
|
||||
|
||||
return {
|
||||
hasEnumConfig,
|
||||
hasEntityReferenceConfig,
|
||||
hasEnumWithDescriptionConfig,
|
||||
};
|
||||
}, [customProperty]);
|
||||
|
||||
@ -116,7 +96,7 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
|
||||
placeholder: t('label.enum-value-plural'),
|
||||
onChange: (value: string[]) => {
|
||||
const enumConfig = customProperty.customPropertyConfig
|
||||
?.config as EnumConfig;
|
||||
?.config as Config;
|
||||
const updatedValues = uniq([...value, ...(enumConfig?.values ?? [])]);
|
||||
form.setFieldsValue({ customPropertyConfig: updatedValues });
|
||||
},
|
||||
@ -175,9 +155,8 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
|
||||
};
|
||||
|
||||
const initialValues = useMemo(() => {
|
||||
if (hasEnumConfig || hasEnumWithDescriptionConfig) {
|
||||
const enumConfig = customProperty.customPropertyConfig
|
||||
?.config as EnumConfig;
|
||||
if (hasEnumConfig) {
|
||||
const enumConfig = customProperty.customPropertyConfig?.config as Config;
|
||||
|
||||
return {
|
||||
description: customProperty.description,
|
||||
@ -190,7 +169,7 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
|
||||
description: customProperty.description,
|
||||
customPropertyConfig: customProperty.customPropertyConfig?.config,
|
||||
};
|
||||
}, [customProperty, hasEnumConfig, hasEnumWithDescriptionConfig]);
|
||||
}, [customProperty, hasEnumConfig]);
|
||||
|
||||
const note = (
|
||||
<Typography.Text
|
||||
@ -244,125 +223,6 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasEnumWithDescriptionConfig && (
|
||||
<>
|
||||
<Form.List name="customPropertyConfig">
|
||||
{(fields, { add, remove }) => {
|
||||
const config =
|
||||
(initialValues?.customPropertyConfig as ValueClass[]) ??
|
||||
[];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
className="form-item-horizontal"
|
||||
colon={false}
|
||||
label={
|
||||
<div className="d-flex gap-2 items-center">
|
||||
<span>{t('label.property')}</span>
|
||||
<Tooltip
|
||||
title={t(
|
||||
'message.enum-with-description-update-note'
|
||||
)}>
|
||||
<InfoCircleOutlined
|
||||
className="m-x-xss"
|
||||
style={{ color: '#C4C4C4' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
}>
|
||||
<Button
|
||||
data-testid="add-enum-description-config"
|
||||
icon={
|
||||
<PlusOutlined
|
||||
style={{ color: 'white', fontSize: '12px' }}
|
||||
/>
|
||||
}
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
add();
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{fields.map((field, index) => {
|
||||
const isExisting = Boolean(get(config, index, false));
|
||||
|
||||
return (
|
||||
<Row
|
||||
className="m-t-md"
|
||||
gutter={[8, 0]}
|
||||
key={field.key}>
|
||||
<Col span={23}>
|
||||
<Row gutter={[8, 0]}>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name={[field.name, 'key']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `${t(
|
||||
'message.field-text-is-required',
|
||||
{
|
||||
fieldText: t('label.key'),
|
||||
}
|
||||
)}`,
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
disabled={isExisting}
|
||||
id={`key-${index}`}
|
||||
placeholder={t('label.key')}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name={[field.name, 'description']}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: `${t(
|
||||
'message.field-text-is-required',
|
||||
{
|
||||
fieldText: t('label.description'),
|
||||
}
|
||||
)}`,
|
||||
},
|
||||
]}
|
||||
trigger="onTextChange"
|
||||
valuePropName="initialValue">
|
||||
<RichTextEditor height="200px" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
{!isExisting && (
|
||||
<Col span={1}>
|
||||
<Button
|
||||
data-testid={`remove-enum-description-config-${index}`}
|
||||
icon={<DeleteIcon width={16} />}
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
|
||||
{generateFormFields([multiSelectField])}
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasEntityReferenceConfig && (
|
||||
<>
|
||||
{generateFormFields([entityReferenceConfigField])}
|
||||
|
||||
@ -77,10 +77,16 @@ export type TimeIntervalType = {
|
||||
end: number;
|
||||
};
|
||||
|
||||
export type TableTypePropertyValueType = {
|
||||
columns: string[];
|
||||
rows: Record<string, string>[];
|
||||
};
|
||||
|
||||
export type PropertyValueType =
|
||||
| string
|
||||
| number
|
||||
| string[]
|
||||
| EntityReference
|
||||
| EntityReference[]
|
||||
| TimeIntervalType;
|
||||
| TimeIntervalType
|
||||
| TableTypePropertyValueType;
|
||||
|
||||
@ -178,12 +178,20 @@ export const CustomPropertyTable = <T extends ExtentionEntitiesKeys>({
|
||||
maxDataCap,
|
||||
]);
|
||||
|
||||
const dataSource = useMemo(() => {
|
||||
const { dataSource, dataSourceColumns } = useMemo(() => {
|
||||
const customProperties = entityTypeDetail?.customProperties ?? [];
|
||||
|
||||
return Array.isArray(customProperties)
|
||||
const dataSource = Array.isArray(customProperties)
|
||||
? customProperties.slice(0, maxDataCap)
|
||||
: [];
|
||||
|
||||
// Split dataSource into three equal parts
|
||||
const columnCount = 3;
|
||||
const columns = Array.from({ length: columnCount }, (_, i) =>
|
||||
dataSource.filter((_, index) => index % columnCount === i)
|
||||
);
|
||||
|
||||
return { dataSource, dataSourceColumns: columns };
|
||||
}, [maxDataCap, entityTypeDetail?.customProperties]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -286,17 +294,21 @@ export const CustomPropertyTable = <T extends ExtentionEntitiesKeys>({
|
||||
</>
|
||||
) : (
|
||||
<Row data-testid="custom-properties-card" gutter={[16, 16]}>
|
||||
{dataSource.map((record) => (
|
||||
<Col key={record.name} span={8}>
|
||||
<PropertyValue
|
||||
extension={extensionObject.extensionObject}
|
||||
hasEditPermissions={hasEditAccess}
|
||||
isRenderedInRightPanel={isRenderedInRightPanel}
|
||||
isVersionView={isVersionView}
|
||||
property={record}
|
||||
versionDataKeys={extensionObject.addedKeysList}
|
||||
onExtensionUpdate={onExtensionUpdate}
|
||||
/>
|
||||
{dataSourceColumns.map((columns, colIndex) => (
|
||||
<Col key={colIndex} span={8}>
|
||||
{columns.map((record) => (
|
||||
<div key={record.name} style={{ marginBottom: '16px' }}>
|
||||
<PropertyValue
|
||||
extension={extensionObject.extensionObject}
|
||||
hasEditPermissions={hasEditAccess}
|
||||
isRenderedInRightPanel={isRenderedInRightPanel}
|
||||
isVersionView={isVersionView}
|
||||
property={record}
|
||||
versionDataKeys={extensionObject.addedKeysList}
|
||||
onExtensionUpdate={onExtensionUpdate}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
@ -26,7 +26,6 @@ import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { AxiosError } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import { t } from 'i18next';
|
||||
@ -60,12 +59,12 @@ import {
|
||||
ICON_DIMENSION,
|
||||
VALIDATION_MESSAGES,
|
||||
} from '../../../constants/constants';
|
||||
import { ENUM_WITH_DESCRIPTION } from '../../../constants/CustomProperty.constants';
|
||||
import { TABLE_TYPE_CUSTOM_PROPERTY } from '../../../constants/CustomProperty.constants';
|
||||
import { TIMESTAMP_UNIX_IN_MILLISECONDS_REGEX } from '../../../constants/regex.constants';
|
||||
import { CSMode } from '../../../enums/codemirror.enum';
|
||||
import { SearchIndex } from '../../../enums/search.enum';
|
||||
import { EntityReference } from '../../../generated/entity/type';
|
||||
import { EnumConfig, ValueClass } from '../../../generated/type/customProperty';
|
||||
import { Config } from '../../../generated/type/customProperty';
|
||||
import { calculateInterval } from '../../../utils/date-time/DateTimeUtils';
|
||||
import entityUtilClassBase from '../../../utils/EntityUtilClassBase';
|
||||
import { getEntityName } from '../../../utils/EntityUtils';
|
||||
@ -78,7 +77,6 @@ import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/Mo
|
||||
import InlineEdit from '../InlineEdit/InlineEdit.component';
|
||||
import ProfilePicture from '../ProfilePicture/ProfilePicture';
|
||||
import RichTextEditorPreviewer from '../RichTextEditor/RichTextEditorPreviewer';
|
||||
import Table from '../Table/Table';
|
||||
import {
|
||||
PropertyValueProps,
|
||||
PropertyValueType,
|
||||
@ -86,6 +84,8 @@ import {
|
||||
} from './CustomPropertyTable.interface';
|
||||
import './property-value.less';
|
||||
import { PropertyInput } from './PropertyInput';
|
||||
import EditTableTypePropertyModal from './TableTypeProperty/EditTableTypePropertyModal';
|
||||
import TableTypePropertyView from './TableTypeProperty/TableTypePropertyView';
|
||||
|
||||
export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
isVersionView,
|
||||
@ -96,9 +96,10 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
property,
|
||||
isRenderedInRightPanel = false,
|
||||
}) => {
|
||||
const { propertyName, propertyType, value } = useMemo(() => {
|
||||
const { propertyName, propertyType, value, isTableType } = useMemo(() => {
|
||||
const propertyName = property.name;
|
||||
const propertyType = property.propertyType;
|
||||
const isTableType = propertyType.name === TABLE_TYPE_CUSTOM_PROPERTY;
|
||||
|
||||
const value = extension?.[propertyName];
|
||||
|
||||
@ -106,13 +107,15 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
propertyName,
|
||||
propertyType,
|
||||
value,
|
||||
isTableType,
|
||||
};
|
||||
}, [property, extension]);
|
||||
|
||||
const [showInput, setShowInput] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
// expand the property value by default if it is a "table-type" custom property
|
||||
const [isExpanded, setIsExpanded] = useState(isTableType);
|
||||
const [isOverflowing, setIsOverflowing] = useState(false);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -135,16 +138,14 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
|
||||
const onInputSave = async (updatedValue: PropertyValueType) => {
|
||||
const isEnum = propertyType.name === 'enum';
|
||||
const isEnumWithDescription = propertyType.name === ENUM_WITH_DESCRIPTION;
|
||||
|
||||
const isArrayType = isArray(updatedValue);
|
||||
|
||||
const enumValue = isArrayType ? updatedValue : [updatedValue];
|
||||
|
||||
const propertyValue =
|
||||
isEnum || isEnumWithDescription
|
||||
? (enumValue as string[]).filter(Boolean)
|
||||
: updatedValue;
|
||||
const propertyValue = isEnum
|
||||
? (enumValue as string[]).filter(Boolean)
|
||||
: updatedValue;
|
||||
|
||||
try {
|
||||
// Omit undefined and empty values
|
||||
@ -227,7 +228,7 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
}
|
||||
|
||||
case 'enum': {
|
||||
const enumConfig = property.customPropertyConfig?.config as EnumConfig;
|
||||
const enumConfig = property.customPropertyConfig?.config as Config;
|
||||
|
||||
const isMultiSelect = Boolean(enumConfig?.multiSelect);
|
||||
|
||||
@ -272,60 +273,6 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
case ENUM_WITH_DESCRIPTION: {
|
||||
const enumConfig = property.customPropertyConfig?.config as EnumConfig;
|
||||
|
||||
const isMultiSelect = Boolean(enumConfig?.multiSelect);
|
||||
|
||||
const values = (enumConfig?.values as ValueClass[]) ?? [];
|
||||
|
||||
const options = values.map((option) => ({
|
||||
label: (
|
||||
<Tooltip title={option.description}>
|
||||
<span>{option.key}</span>
|
||||
</Tooltip>
|
||||
),
|
||||
value: option.key,
|
||||
}));
|
||||
|
||||
const initialValues = {
|
||||
enumWithDescriptionValues: (isArray(value) ? value : [value]).filter(
|
||||
Boolean
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<InlineEdit
|
||||
isLoading={isLoading}
|
||||
saveButtonProps={{
|
||||
disabled: isLoading,
|
||||
htmlType: 'submit',
|
||||
form: 'enum-with-description-form',
|
||||
}}
|
||||
onCancel={onHideInput}
|
||||
onSave={noop}>
|
||||
<Form
|
||||
id="enum-with-description-form"
|
||||
initialValues={initialValues}
|
||||
layout="vertical"
|
||||
onFinish={(values: {
|
||||
enumWithDescriptionValues: string | string[];
|
||||
}) => onInputSave(values.enumWithDescriptionValues)}>
|
||||
<Form.Item name="enumWithDescriptionValues" style={commonStyle}>
|
||||
<Select
|
||||
allowClear
|
||||
data-testid="enum-with-description-select"
|
||||
disabled={isLoading}
|
||||
mode={isMultiSelect ? 'multiple' : undefined}
|
||||
options={options}
|
||||
placeholder={t('label.enum-value-plural')}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</InlineEdit>
|
||||
);
|
||||
}
|
||||
|
||||
case 'date-cp':
|
||||
case 'dateTime-cp': {
|
||||
// Default format is 'yyyy-mm-dd'
|
||||
@ -748,6 +695,31 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
case TABLE_TYPE_CUSTOM_PROPERTY: {
|
||||
const config = property.customPropertyConfig?.config as Config;
|
||||
|
||||
const columns = config?.columns ?? [];
|
||||
const rows = value?.rows ?? [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{showInput && (
|
||||
<TableTypePropertyView columns={columns} rows={rows} />
|
||||
)}
|
||||
<EditTableTypePropertyModal
|
||||
columns={columns}
|
||||
isUpdating={isLoading}
|
||||
isVisible={showInput}
|
||||
maxRowCount={config?.rowCount ?? 10}
|
||||
property={property}
|
||||
rows={value?.rows ?? []}
|
||||
onCancel={onHideInput}
|
||||
onSave={onInputSave}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -791,42 +763,6 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
</>
|
||||
);
|
||||
|
||||
case ENUM_WITH_DESCRIPTION: {
|
||||
const enumWithDescriptionValues = (value as ValueClass[]) ?? [];
|
||||
|
||||
const columns: ColumnsType<ValueClass> = [
|
||||
{
|
||||
title: 'Key',
|
||||
dataIndex: 'key',
|
||||
key: 'key',
|
||||
render: (key: string) => <Typography>{key}</Typography>,
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
render: (description: string) => (
|
||||
<RichTextEditorPreviewer markdown={description || ''} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Table
|
||||
bordered
|
||||
resizableColumns
|
||||
className="w-full"
|
||||
columns={columns}
|
||||
data-testid="enum-with-description-table"
|
||||
dataSource={enumWithDescriptionValues}
|
||||
pagination={false}
|
||||
rowKey="name"
|
||||
scroll={isRenderedInRightPanel ? { x: true } : undefined}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'sqlQuery':
|
||||
return (
|
||||
<SchemaEditor
|
||||
@ -981,6 +917,14 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
case TABLE_TYPE_CUSTOM_PROPERTY: {
|
||||
const columns =
|
||||
(property.customPropertyConfig?.config as Config)?.columns ?? [];
|
||||
const rows = value?.rows ?? [];
|
||||
|
||||
return <TableTypePropertyView columns={columns} rows={rows} />;
|
||||
}
|
||||
|
||||
case 'string':
|
||||
case 'integer':
|
||||
case 'number':
|
||||
@ -1004,7 +948,8 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
const getValueElement = () => {
|
||||
const propertyValue = getPropertyValue();
|
||||
|
||||
return !isUndefined(value) ? (
|
||||
// if value is not undefined or property is a table type(at least show the columns), return the property value
|
||||
return !isUndefined(value) || isTableType ? (
|
||||
propertyValue
|
||||
) : (
|
||||
<span className="text-grey-muted" data-testid="no-data">
|
||||
@ -1089,7 +1034,7 @@ export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
ref={contentRef}
|
||||
style={{
|
||||
height: containerStyleFlag ? 'auto' : '30px',
|
||||
overflow: containerStyleFlag ? 'visible' : 'hidden',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
{showInput ? getPropertyInput() : getValueElement()}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,236 @@
|
||||
/*
|
||||
* 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 ReactDataGrid from '@inovua/reactdatagrid-community';
|
||||
import '@inovua/reactdatagrid-community/index.css';
|
||||
import { TypeComputedProps } from '@inovua/reactdatagrid-community/types';
|
||||
import { Button, Modal, Tooltip, Typography } from 'antd';
|
||||
import { isEmpty, omit } from 'lodash';
|
||||
import React, { FC, MutableRefObject, useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CustomProperty } from '../../../../generated/type/customProperty';
|
||||
import { TableTypePropertyValueType } from '../CustomPropertyTable.interface';
|
||||
import './edit-table-type-property.less';
|
||||
import TableTypePropertyView from './TableTypePropertyView';
|
||||
|
||||
interface EditTableTypePropertyModalProps {
|
||||
isVisible: boolean;
|
||||
isUpdating: boolean;
|
||||
property: CustomProperty;
|
||||
columns: string[];
|
||||
rows: Record<string, string>[];
|
||||
maxRowCount: number;
|
||||
onCancel: () => void;
|
||||
onSave: (data: TableTypePropertyValueType) => Promise<void>;
|
||||
}
|
||||
|
||||
let inEdit = false;
|
||||
|
||||
const EditTableTypePropertyModal: FC<EditTableTypePropertyModalProps> = ({
|
||||
isVisible,
|
||||
isUpdating,
|
||||
property,
|
||||
columns,
|
||||
rows,
|
||||
maxRowCount,
|
||||
onCancel,
|
||||
onSave,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [dataSource, setDataSource] = useState<
|
||||
TableTypePropertyValueType['rows']
|
||||
>(() => rows.map((row, index) => ({ ...row, id: index + '' })));
|
||||
|
||||
const [gridRef, setGridRef] = useState<
|
||||
MutableRefObject<TypeComputedProps | null>
|
||||
>({ current: null });
|
||||
|
||||
const filterColumns = columns.map((column) => ({
|
||||
name: column,
|
||||
header: column,
|
||||
defaultFlex: 1,
|
||||
sortable: false,
|
||||
minWidth: 180,
|
||||
}));
|
||||
|
||||
const onEditComplete = useCallback(
|
||||
({ value, columnId, rowId }) => {
|
||||
const data = [...dataSource];
|
||||
|
||||
data[rowId][columnId] = value;
|
||||
|
||||
setDataSource(data);
|
||||
},
|
||||
[dataSource]
|
||||
);
|
||||
|
||||
const onEditStart = () => {
|
||||
inEdit = true;
|
||||
};
|
||||
|
||||
const onEditStop = () => {
|
||||
requestAnimationFrame(() => {
|
||||
inEdit = false;
|
||||
gridRef.current?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (inEdit) {
|
||||
if (event.key === 'Escape') {
|
||||
const [rowIndex, colIndex] = gridRef.current?.computedActiveCell ?? [
|
||||
0, 0,
|
||||
];
|
||||
const column = gridRef.current?.getColumnBy(colIndex);
|
||||
|
||||
gridRef.current?.cancelEdit?.({
|
||||
rowIndex,
|
||||
columnId: column?.name ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
const grid = gridRef.current;
|
||||
if (!grid) {
|
||||
return;
|
||||
}
|
||||
let [rowIndex, colIndex] = grid.computedActiveCell ?? [0, 0];
|
||||
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
const column = grid.getColumnBy(colIndex);
|
||||
grid.startEdit?.({ columnId: column.name ?? '', rowIndex });
|
||||
event.preventDefault();
|
||||
|
||||
return;
|
||||
}
|
||||
if (event.key !== 'Tab') {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const direction = event.shiftKey ? -1 : 1;
|
||||
|
||||
const columns = grid.visibleColumns;
|
||||
const rowCount = grid.count;
|
||||
|
||||
colIndex += direction;
|
||||
if (colIndex === -1) {
|
||||
colIndex = columns.length - 1;
|
||||
rowIndex -= 1;
|
||||
}
|
||||
if (colIndex === columns.length) {
|
||||
rowIndex += 1;
|
||||
colIndex = 0;
|
||||
}
|
||||
if (rowIndex < 0 || rowIndex === rowCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
grid?.setActiveCell([rowIndex, colIndex]);
|
||||
};
|
||||
|
||||
const handleAddRow = useCallback(() => {
|
||||
setDataSource((data) => {
|
||||
setTimeout(() => {
|
||||
gridRef.current?.scrollToId(data.length + '');
|
||||
gridRef.current?.focus();
|
||||
}, 1);
|
||||
|
||||
return [...data, { id: data.length + '' }];
|
||||
});
|
||||
}, [gridRef]);
|
||||
|
||||
const handleUpdate = useCallback(async () => {
|
||||
const modifiedRows = dataSource
|
||||
.map((row) => omit(row, 'id'))
|
||||
// if the row is empty, filter it out
|
||||
.filter((row) => !isEmpty(row) && Object.values(row).some(Boolean));
|
||||
await onSave({ rows: modifiedRows, columns });
|
||||
}, [onSave, dataSource, columns]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
centered
|
||||
destroyOnClose
|
||||
closable={false}
|
||||
data-testid="edit-table-type-property-modal"
|
||||
footer={
|
||||
<div className="d-flex justify-between">
|
||||
<Tooltip
|
||||
title={
|
||||
dataSource.length === maxRowCount
|
||||
? t('message.maximum-count-allowed', {
|
||||
count: maxRowCount,
|
||||
label: t('label.row-plural'),
|
||||
})
|
||||
: t('label.add-entity', { entity: t('label.row') })
|
||||
}>
|
||||
<Button
|
||||
disabled={dataSource.length === maxRowCount || isUpdating}
|
||||
type="primary"
|
||||
onClick={handleAddRow}>
|
||||
{t('label.add-entity', { entity: t('label.row') })}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<div className="d-flex gap-2">
|
||||
<Button disabled={isUpdating} onClick={onCancel}>
|
||||
{t('label.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isUpdating}
|
||||
loading={isUpdating}
|
||||
type="primary"
|
||||
onClick={handleUpdate}>
|
||||
{t('label.update')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
maskClosable={false}
|
||||
open={isVisible}
|
||||
title={
|
||||
<Typography.Text>
|
||||
{t('label.edit-entity-name', {
|
||||
entityType: t('label.property'),
|
||||
entityName: property.name,
|
||||
})}
|
||||
</Typography.Text>
|
||||
}
|
||||
width={800}>
|
||||
{isEmpty(dataSource) ? (
|
||||
<TableTypePropertyView isInModal columns={columns} rows={rows} />
|
||||
) : (
|
||||
<ReactDataGrid
|
||||
editable
|
||||
className="edit-table-type-property"
|
||||
columns={filterColumns}
|
||||
dataSource={dataSource}
|
||||
handle={setGridRef}
|
||||
idProperty="id"
|
||||
minRowHeight={30}
|
||||
showZebraRows={false}
|
||||
style={{ height: '180px' }}
|
||||
onEditComplete={onEditComplete}
|
||||
onEditStart={onEditStart}
|
||||
onEditStop={onEditStop}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditTableTypePropertyModal;
|
||||
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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 { isArray } from 'lodash';
|
||||
import React, { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NO_DATA_PLACEHOLDER } from '../../../../constants/constants';
|
||||
import Table from '../../Table/Table';
|
||||
import './table-type-property-view.less';
|
||||
|
||||
interface TableTypePropertyViewProps {
|
||||
columns: string[];
|
||||
rows: Record<string, string>[];
|
||||
isInModal?: boolean;
|
||||
}
|
||||
|
||||
const TableTypePropertyView: FC<TableTypePropertyViewProps> = ({
|
||||
columns,
|
||||
rows,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isArray(columns) || !isArray(rows)) {
|
||||
return (
|
||||
<span className="text-grey-muted" data-testid="invalid-data">
|
||||
{t('label.field-invalid', {
|
||||
field: `${t('label.column-plural')} or ${t('label.row-plural')}`,
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const tableColumns = columns.map((column: string) => ({
|
||||
title: column,
|
||||
dataIndex: column,
|
||||
key: column,
|
||||
render: (text: string) => text ?? NO_DATA_PLACEHOLDER,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Table
|
||||
bordered
|
||||
resizableColumns
|
||||
className="w-full table-type-custom-property"
|
||||
columns={tableColumns}
|
||||
data-testid="table-type-property-value"
|
||||
dataSource={rows}
|
||||
pagination={false}
|
||||
rowKey="name"
|
||||
scroll={{ x: true }}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableTypePropertyView;
|
||||
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 url('../../../../styles/variables.less');
|
||||
|
||||
.edit-table-type-property {
|
||||
.InovuaReactDataGrid__column-header:hover {
|
||||
.InovuaReactDataGrid__column-header__menu-tool {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.InovuaReactDataGrid__header {
|
||||
border-top-right-radius: 6px;
|
||||
border-top-left-radius: 6px;
|
||||
font-weight: 500;
|
||||
color: @grey-4;
|
||||
text-transform: uppercase;
|
||||
background: @grey-1;
|
||||
.InovuaReactDataGrid__column-header__content {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.InovuaReactDataGrid__virtual-list {
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
.InovuaReactDataGrid__row-cell-wrap {
|
||||
color: @text-color;
|
||||
}
|
||||
|
||||
border: 1px solid @border-color;
|
||||
border-radius: 6px;
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
@border-right-color: #0000000f;
|
||||
|
||||
.table-type-custom-property.ant-table-wrapper {
|
||||
.ant-table-bordered {
|
||||
.ant-table-container {
|
||||
.ant-table-content > table {
|
||||
tbody > tr {
|
||||
td {
|
||||
border-right: 1px solid @border-right-color;
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
thead > tr {
|
||||
th {
|
||||
border-right: 1px solid @border-right-color;
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -86,3 +86,9 @@
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.property-description {
|
||||
.markdown-parser .toastui-editor-contents {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,4 +127,4 @@ export const SUPPORTED_FORMAT_MAP = {
|
||||
'time-cp': SUPPORTED_TIME_FORMATS,
|
||||
};
|
||||
|
||||
export const ENUM_WITH_DESCRIPTION = 'enumWithDescriptions';
|
||||
export const TABLE_TYPE_CUSTOM_PROPERTY = 'table-cp';
|
||||
|
||||
@ -983,6 +983,7 @@
|
||||
"role-name": "Rollenname",
|
||||
"role-plural": "Rollen",
|
||||
"row": "Zeile",
|
||||
"row-count": "Row Count",
|
||||
"row-count-lowercase": "Anzahl der Zeilen",
|
||||
"row-plural": "Zeilen",
|
||||
"rtl-ltr-direction": "RTL/LTR direction",
|
||||
@ -1591,6 +1592,7 @@
|
||||
"mark-deleted-table-message": "Dies ist eine optionale Konfiguration zur Aktivierung des sanften Löschens von Tabellen. Wenn diese Option aktiviert ist, werden nur Tabellen, die aus der Quelle gelöscht wurden, sanft gelöscht, und dies gilt ausschließlich für das Schema, das gerade über die Pipeline erfasst wird. Alle zugehörigen Entitäten wie Test-Suiten oder Verbindungsdaten, die mit diesen Tabellen verknüpft waren, werden ebenfalls gelöscht.",
|
||||
"markdown-editor-placeholder": "Verwenden Sie @Erwähnen, um einen Benutzer oder ein Team zu kennzeichnen.\nVerwenden Sie #Erwähnen, um eine Datenressource zu kennzeichnen.",
|
||||
"marketplace-verify-msg": "OpenMetadata has verified that the publisher controls the domain and meets other requirements.",
|
||||
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
|
||||
"maximum-value-error": "The maximum value should be greater than the minimum value.",
|
||||
"member-description": "Streamline access to users and teams in OpenMetadata.",
|
||||
"mentioned-you-on-the-lowercase": "hat dich auf der",
|
||||
|
||||
@ -983,6 +983,7 @@
|
||||
"role-name": "Role Name",
|
||||
"role-plural": "Roles",
|
||||
"row": "Row",
|
||||
"row-count": "Row Count",
|
||||
"row-count-lowercase": "row count",
|
||||
"row-plural": "Rows",
|
||||
"rtl-ltr-direction": "RTL/LTR direction",
|
||||
@ -1591,6 +1592,7 @@
|
||||
"mark-deleted-table-message": "This is an optional configuration for enabling soft deletion of tables. When this option is enabled, only tables that have been deleted from the source will be soft deleted, and this will apply solely to the schema that is currently being ingested via the pipeline. Any related entities such as test suites or lineage information that were associated with those tables will also be deleted.",
|
||||
"markdown-editor-placeholder": "Use @mention to tag a user or a team.\nUse #mention to tag a data asset.",
|
||||
"marketplace-verify-msg": "OpenMetadata has verified that the publisher controls the domain and meets other requirements.",
|
||||
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
|
||||
"maximum-value-error": "The maximum value should be greater than the minimum value.",
|
||||
"member-description": "Streamline access to users and teams in OpenMetadata.",
|
||||
"mentioned-you-on-the-lowercase": "mentioned you on the",
|
||||
|
||||
@ -983,6 +983,7 @@
|
||||
"role-name": "Nombre del Rol",
|
||||
"role-plural": "Roles",
|
||||
"row": "Fila",
|
||||
"row-count": "Row Count",
|
||||
"row-count-lowercase": "número de filas",
|
||||
"row-plural": "Filas",
|
||||
"rtl-ltr-direction": "dirección RTL/LTR",
|
||||
@ -1591,6 +1592,7 @@
|
||||
"mark-deleted-table-message": "Esta es una configuración opcional para habilitar la eliminación suave de tablas. Cuando se habilita esta opción, sólo se eliminarán suavemente las tablas que se hayan eliminado de la fuente, y esto se aplicará únicamente al esquema que se esté ingiriendo actualmente a través de la canalización. Cualquier entidad relacionada, como tests o información de linaje que estuvieran asociados con esas tablas, también se eliminarán.",
|
||||
"markdown-editor-placeholder": "Usa @mención para etiquetar a un usuario o equipo.\n Usa #mención para etiquetar un activo de datos.",
|
||||
"marketplace-verify-msg": "OpenMetadata ha verificado que el editor controla el dominio y cumple con otros requisitos.",
|
||||
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
|
||||
"maximum-value-error": "The maximum value should be greater than the minimum value.",
|
||||
"member-description": "Optimiza el acceso a usuarios y equipos en OpenMetadata.",
|
||||
"mentioned-you-on-the-lowercase": "te mencionó en",
|
||||
|
||||
@ -983,6 +983,7 @@
|
||||
"role-name": "Nom du Rôle",
|
||||
"role-plural": "Rôles",
|
||||
"row": "Ligne",
|
||||
"row-count": "Row Count",
|
||||
"row-count-lowercase": "Nombre de Ligne",
|
||||
"row-plural": "Lignes",
|
||||
"rtl-ltr-direction": "RTL/LTR direction",
|
||||
@ -1591,6 +1592,7 @@
|
||||
"mark-deleted-table-message": "Il s'agit d'une configuration optionnelle permettant de permettre la suppression douce des tables. Lorsque cette option est activée, seules les tables supprimées de la source seront supprimées en douceur, et cela s'appliquera uniquement au schéma actuellement ingéré via le pipeline. Toute entité associée telle que les suites de tests ou les informations de lignée qui étaient associées à ces tables sera également supprimée.",
|
||||
"markdown-editor-placeholder": "Utilisez @mention pour identifier un utilisateur ou une équipe.\nUtilisez #mention pour identifier un actif de données.",
|
||||
"marketplace-verify-msg": "OpenMetadata has verified that the publisher controls the domain and meets other requirements.",
|
||||
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
|
||||
"maximum-value-error": "La valeur maximum doit être plus grande que la valeur minimum.",
|
||||
"member-description": "Gérez les accès des utilisateurs et équipes à OpenMetadata.",
|
||||
"mentioned-you-on-the-lowercase": "vous a mentionné dans",
|
||||
|
||||
@ -983,6 +983,7 @@
|
||||
"role-name": "שם התפקיד",
|
||||
"role-plural": "תפקידים",
|
||||
"row": "שורה",
|
||||
"row-count": "Row Count",
|
||||
"row-count-lowercase": "מספר שורות",
|
||||
"row-plural": "שורות",
|
||||
"rtl-ltr-direction": "RTL/LTR direction",
|
||||
@ -1591,6 +1592,7 @@
|
||||
"mark-deleted-table-message": "זו הגדרת אופציונלית לאפשר מחיקה רכה של טבלאות. כאשר האפשרות מופעלת, רק טבלאות שנמחקו ממקור הנתונים יימחקו רכה, וזה יחול באופן בלעדי על הסכמה הנמצאת כעת בתהליך קליטת הנתונים דרך הצנרת נתונים. יימחקו גם יישויות קשורות כמו סוויטי ניסויים או מידע של שקפיות המשוייכים לטבלאות אלו.",
|
||||
"markdown-editor-placeholder": "השתמש ב-@תייג לתייג משתמש או צוות.\nהשתמש ב-#תייג לתייג נכס נתונים.",
|
||||
"marketplace-verify-msg": "OpenMetadata אישרה שהמוציא לפועל שולט בדומיין ועומד בדרישות האחרות.",
|
||||
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
|
||||
"maximum-value-error": "The maximum value should be greater than the minimum value.",
|
||||
"member-description": "Streamline access to users and teams in OpenMetadata.",
|
||||
"mentioned-you-on-the-lowercase": "ציין אותך ב",
|
||||
|
||||
@ -983,6 +983,7 @@
|
||||
"role-name": "ロール名",
|
||||
"role-plural": "ロール",
|
||||
"row": "行",
|
||||
"row-count": "Row Count",
|
||||
"row-count-lowercase": "行数",
|
||||
"row-plural": "行",
|
||||
"rtl-ltr-direction": "RTL/LTR direction",
|
||||
@ -1591,6 +1592,7 @@
|
||||
"mark-deleted-table-message": "This is an optional configuration for enabling soft deletion of tables. When this option is enabled, only tables that have been deleted from the source will be soft deleted, and this will apply solely to the schema that is currently being ingested via the pipeline. Any related entities such as test suites or lineage information that were associated with those tables will also be deleted.",
|
||||
"markdown-editor-placeholder": "@mention を使ってユーザやチームをタグ付けしましょう。\n#mentionを使うとデータアセットにタグ付けできます。",
|
||||
"marketplace-verify-msg": "OpenMetadata has verified that the publisher controls the domain and meets other requirements.",
|
||||
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
|
||||
"maximum-value-error": "The maximum value should be greater than the minimum value.",
|
||||
"member-description": "Streamline access to users and teams in OpenMetadata.",
|
||||
"mentioned-you-on-the-lowercase": "mentioned you on the",
|
||||
|
||||
@ -983,6 +983,7 @@
|
||||
"role-name": "Rolnaam",
|
||||
"role-plural": "Rollen",
|
||||
"row": "Rij",
|
||||
"row-count": "Row Count",
|
||||
"row-count-lowercase": "aantal rijen",
|
||||
"row-plural": "Rijen",
|
||||
"rtl-ltr-direction": "RTL/LTR-richting",
|
||||
@ -1591,6 +1592,7 @@
|
||||
"mark-deleted-table-message": "Dit is een optionele configuratie voor het inschakelen van zachte verwijdering van tabellen. Wanneer deze optie is ingeschakeld, worden alleen tabellen die zijn verwijderd uit de bron zacht verwijderd, en dit is uitsluitend van toepassing op het schema dat momenteel wordt ingenomen via de pipeline. Elke gerelateerde entiteit, zoals testsuites of lineage-informatie, die met die tabellen was geassocieerd, wordt ook verwijderd.",
|
||||
"markdown-editor-placeholder": "Gebruik @mention om een gebruiker of een team te taggen.\nGebruik #mention om een data-asset te taggen.",
|
||||
"marketplace-verify-msg": "OpenMetadata heeft geverifieerd dat de publisher het domein beheert en aan andere eisen voldoet.",
|
||||
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
|
||||
"maximum-value-error": "The maximum value should be greater than the minimum value.",
|
||||
"member-description": "Stroomlijn toegang voor gebruikers en teams in OpenMetadata.",
|
||||
"mentioned-you-on-the-lowercase": "noemde je op de",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -983,6 +983,7 @@
|
||||
"role-name": "Nome da Função",
|
||||
"role-plural": "Funções",
|
||||
"row": "Linha",
|
||||
"row-count": "Row Count",
|
||||
"row-count-lowercase": "contagem de linhas",
|
||||
"row-plural": "Linhas",
|
||||
"rtl-ltr-direction": "Direção RTL/LTR",
|
||||
@ -1591,6 +1592,7 @@
|
||||
"mark-deleted-table-message": "Esta é uma configuração opcional para habilitar a exclusão suave de tabelas. Quando esta opção está habilitada, apenas as tabelas que foram excluídas da fonte serão excluídas suavemente, e isso se aplicará somente ao esquema que está sendo ingerido atualmente pelo pipeline. Quaisquer entidades relacionadas, como conjuntos de teste ou informações de linhagem que estavam associadas a essas tabelas, também serão excluídas.",
|
||||
"markdown-editor-placeholder": "Use @mention para marcar um usuário ou uma equipe.\nUse #mention para marcar um ativo de dados.",
|
||||
"marketplace-verify-msg": "O OpenMetadata verificou que o editor controla o domínio e atende a outros requisitos.",
|
||||
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
|
||||
"maximum-value-error": "The maximum value should be greater than the minimum value.",
|
||||
"member-description": "Streamline access to users and teams in OpenMetadata.",
|
||||
"mentioned-you-on-the-lowercase": "mencionou você no",
|
||||
|
||||
@ -983,6 +983,7 @@
|
||||
"role-name": "Наименование роли",
|
||||
"role-plural": "Роли",
|
||||
"row": "Строка",
|
||||
"row-count": "Row Count",
|
||||
"row-count-lowercase": "количество строк",
|
||||
"row-plural": "Строки",
|
||||
"rtl-ltr-direction": "RTL/LTR direction",
|
||||
@ -1591,6 +1592,7 @@
|
||||
"mark-deleted-table-message": "Это необязательная конфигурация для включения обратимого удаления таблиц. Если этот параметр включен, обратимому удалению подлежат только таблицы, которые были удалены из источника, и это будет применяться исключительно к схеме, которая в данный момент загружается через конвейер. Любые связанные объекты, такие как наборы тестов или информация о происхождении, которые были связаны с этими таблицами, также будут удалены.",
|
||||
"markdown-editor-placeholder": "Используйте @упоминание, чтобы отметить пользователя или команду.\nИспользуйте #упоминание, чтобы пометить объект данных.",
|
||||
"marketplace-verify-msg": "OpenMetadata has verified that the publisher controls the domain and meets other requirements.",
|
||||
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
|
||||
"maximum-value-error": "The maximum value should be greater than the minimum value.",
|
||||
"member-description": "Streamline access to users and teams in OpenMetadata.",
|
||||
"mentioned-you-on-the-lowercase": "упоминул вас в",
|
||||
|
||||
@ -983,6 +983,7 @@
|
||||
"role-name": "角色名",
|
||||
"role-plural": "角色",
|
||||
"row": "行",
|
||||
"row-count": "Row Count",
|
||||
"row-count-lowercase": "行计数",
|
||||
"row-plural": "行",
|
||||
"rtl-ltr-direction": "RTL/LTR direction",
|
||||
@ -1591,6 +1592,7 @@
|
||||
"mark-deleted-table-message": "可选配置, 用于启用数据表的软删除。只有在数据源中被删除的表才会被软删除, 而且仅会作用到当前被工作流提取进来的 Schema(模式)。所有这些数据表的关联实体都将被删除, 例如测试集和血缘信息等。",
|
||||
"markdown-editor-placeholder": "使用 @提及 标记用户或团队。\n使用 #提及 标记数据资产",
|
||||
"marketplace-verify-msg": "OpenMetadata 已核实发布者控制该域并符合其他要求",
|
||||
"maximum-count-allowed": "Maximum {{count}} {{label}} are allowed",
|
||||
"maximum-value-error": "最大值应大于最小值",
|
||||
"member-description": "简化对 OpenMetadata 用户和团队的访问",
|
||||
"mentioned-you-on-the-lowercase": "在以下地方提到了您",
|
||||
|
||||
@ -19,15 +19,12 @@ import {
|
||||
ExtensionDataProps,
|
||||
ExtensionDataTypes,
|
||||
} from '../../components/Modals/ModalWithCustomProperty/ModalWithMarkdownEditor.interface';
|
||||
import { NO_DATA_PLACEHOLDER } from '../../constants/constants';
|
||||
import { SEMICOLON_SPLITTER } from '../../constants/regex.constants';
|
||||
import { EntityType } from '../../enums/entity.enum';
|
||||
import {
|
||||
CustomProperty,
|
||||
EntityReference,
|
||||
EnumConfig,
|
||||
Type,
|
||||
ValueClass,
|
||||
} from '../../generated/entity/type';
|
||||
import { Status } from '../../generated/type/csvImportResult';
|
||||
import { removeOuterEscapes } from '../CommonUtils';
|
||||
@ -193,21 +190,6 @@ const convertCustomPropertyStringToValueExtensionBasedOnType = (
|
||||
}
|
||||
}
|
||||
|
||||
case 'enumWithDescriptions': {
|
||||
const propertyEnumValues =
|
||||
((customProperty?.customPropertyConfig?.config as EnumConfig)
|
||||
.values as ValueClass[]) ?? [];
|
||||
|
||||
const keyAndValue: Record<string, ValueClass> = {};
|
||||
|
||||
propertyEnumValues.forEach((cp) => (keyAndValue[cp.key] = cp));
|
||||
|
||||
return value.split('|').map((item) => ({
|
||||
key: item,
|
||||
description: keyAndValue[item].description ?? NO_DATA_PLACEHOLDER,
|
||||
}));
|
||||
}
|
||||
|
||||
case 'timeInterval': {
|
||||
const [start, end] = value.split(':');
|
||||
|
||||
@ -246,11 +228,6 @@ const convertCustomPropertyValueExtensionToStringBasedOnType = (
|
||||
case 'enum':
|
||||
return (value as unknown as string[]).map((item) => item).join('|');
|
||||
|
||||
case 'enumWithDescriptions':
|
||||
return (value as unknown as ValueClass[])
|
||||
.map((item) => item.key)
|
||||
.join('|');
|
||||
|
||||
case 'timeInterval': {
|
||||
const interval = value as { start: string; end: string };
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user