mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-26 01:15:08 +00:00
MINOR - Fix updates & add rule provider (#22676)
* MINOR - Fix updates & add rule provider * increrase the rule modal width and remove the rule column from table component in settings * disabled edit and delete if system provider and change the rule logic * format * fix css of contract detail page and form component * fix tests * add the ability to ignore entities --------- Co-authored-by: Ashish Gupta <ashish@getcollate.io> Co-authored-by: Sriharsha Chintalapani <harshach@users.noreply.github.com>
This commit is contained in:
parent
fe28faa13f
commit
30e2f83d50
@ -23,6 +23,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
@ -630,6 +631,59 @@ public class DataContractRepository extends EntityRepository<DataContract> {
|
||||
@Override
|
||||
public void entitySpecificUpdate(boolean consolidatingChanges) {
|
||||
recordChange("latestResult", original.getLatestResult(), updated.getLatestResult());
|
||||
recordChange("status", original.getStatus(), updated.getStatus());
|
||||
recordChange("testSuite", original.getTestSuite(), updated.getTestSuite());
|
||||
updateSchema(original, updated);
|
||||
updateQualityExpectations(original, updated);
|
||||
updateSemantics(original, updated);
|
||||
}
|
||||
|
||||
private void updateSchema(DataContract original, DataContract updated) {
|
||||
List<Column> addedColumns = new ArrayList<>();
|
||||
List<Column> deletedColumns = new ArrayList<>();
|
||||
recordListChange(
|
||||
"schema",
|
||||
original.getSchema(),
|
||||
updated.getSchema(),
|
||||
addedColumns,
|
||||
deletedColumns,
|
||||
EntityUtil.columnMatch);
|
||||
}
|
||||
|
||||
private void updateQualityExpectations(DataContract original, DataContract updated) {
|
||||
List<EntityReference> addedQualityExpectations = new ArrayList<>();
|
||||
List<EntityReference> deletedQualityExpectations = new ArrayList<>();
|
||||
recordListChange(
|
||||
"qualityExpectations",
|
||||
original.getQualityExpectations(),
|
||||
updated.getQualityExpectations(),
|
||||
addedQualityExpectations,
|
||||
deletedQualityExpectations,
|
||||
EntityUtil.entityReferenceMatch);
|
||||
}
|
||||
|
||||
private void updateSemantics(DataContract original, DataContract updated) {
|
||||
List<SemanticsRule> addedSemantics = new ArrayList<>();
|
||||
List<SemanticsRule> deletedSemantics = new ArrayList<>();
|
||||
recordListChange(
|
||||
"semantics",
|
||||
original.getSemantics(),
|
||||
updated.getSemantics(),
|
||||
addedSemantics,
|
||||
deletedSemantics,
|
||||
this::semanticsRuleMatch);
|
||||
}
|
||||
|
||||
private boolean semanticsRuleMatch(SemanticsRule rule1, SemanticsRule rule2) {
|
||||
if (rule1 == null || rule2 == null) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(rule1.getName(), rule2.getName())
|
||||
&& Objects.equals(rule1.getRule(), rule2.getRule())
|
||||
&& Objects.equals(rule1.getDescription(), rule2.getDescription())
|
||||
&& Objects.equals(rule1.getEnabled(), rule2.getEnabled())
|
||||
&& Objects.equals(rule1.getEntityType(), rule2.getEntityType())
|
||||
&& Objects.equals(rule1.getProvider(), rule2.getProvider());
|
||||
}
|
||||
}
|
||||
|
||||
@ -661,6 +715,15 @@ public class DataContractRepository extends EntityRepository<DataContract> {
|
||||
DataContract.class);
|
||||
}
|
||||
|
||||
public DataContract getEntityDataContractSafely(EntityInterface entity) {
|
||||
try {
|
||||
return loadEntityDataContract(entity.getEntityReference());
|
||||
} catch (Exception e) {
|
||||
LOG.debug("Failed to load data contracts for entity {}: {}", entity.getId(), e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeEntity(DataContract dataContract, boolean update) {
|
||||
store(dataContract, update);
|
||||
|
@ -62,6 +62,7 @@ import org.openmetadata.sdk.PipelineServiceClientInterface;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.OpenMetadataApplicationConfig;
|
||||
import org.openmetadata.service.clients.pipeline.PipelineServiceClientFactory;
|
||||
import org.openmetadata.service.exception.EntityNotFoundException;
|
||||
import org.openmetadata.service.jdbi3.DataContractRepository;
|
||||
import org.openmetadata.service.jdbi3.EntityTimeSeriesDAO;
|
||||
import org.openmetadata.service.jdbi3.ListFilter;
|
||||
@ -302,6 +303,10 @@ public class DataContractResource extends EntityResource<DataContract, DataContr
|
||||
DataContract dataContract =
|
||||
repository.loadEntityDataContract(
|
||||
new EntityReference().withId(entityId).withType(entityType));
|
||||
if (dataContract == null) {
|
||||
throw EntityNotFoundException.byMessage(
|
||||
String.format("Data contract for entity %s is not found", entityId));
|
||||
}
|
||||
return addHref(uriInfo, repository.setFieldsInternal(dataContract, getFields(fieldsParam)));
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ import org.openmetadata.schema.type.SemanticsRule;
|
||||
import org.openmetadata.schema.utils.JsonUtils;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.jdbi3.DataContractRepository;
|
||||
import org.openmetadata.service.jdbi3.EntityRepository;
|
||||
import org.openmetadata.service.resources.settings.SettingsCache;
|
||||
|
||||
@Slf4j
|
||||
@ -74,8 +75,10 @@ public class RuleEngine {
|
||||
ArrayList<SemanticsRule> rulesToEvaluate = new ArrayList<>();
|
||||
if (!incomingOnly) {
|
||||
rulesToEvaluate.addAll(getEnabledEntitySemantics());
|
||||
DataContract entityContract = getEntityDataContractSafely(facts);
|
||||
if (entityContract != null && entityContract.getStatus() == ContractStatus.Active) {
|
||||
DataContract entityContract = dataContractRepository.getEntityDataContractSafely(facts);
|
||||
if (entityContract != null
|
||||
&& entityContract.getStatus() == ContractStatus.Active
|
||||
&& !nullOrEmpty(entityContract.getSemantics())) {
|
||||
rulesToEvaluate.addAll(entityContract.getSemantics());
|
||||
}
|
||||
}
|
||||
@ -90,12 +93,7 @@ public class RuleEngine {
|
||||
List<SemanticsRule> erroredRules = new ArrayList<>();
|
||||
rulesToEvaluate.forEach(
|
||||
rule -> {
|
||||
// Only evaluate the rule if it's a generic rule or the rule's entity type matches the
|
||||
// facts class
|
||||
if (rule.getEntityType() == null
|
||||
|| Entity.getEntityRepository(rule.getEntityType())
|
||||
.getEntityClass()
|
||||
.isInstance(facts)) {
|
||||
if (shouldApplyRule(facts, rule)) {
|
||||
try {
|
||||
validateRule(facts, rule);
|
||||
} catch (RuleValidationException e) {
|
||||
@ -107,6 +105,27 @@ public class RuleEngine {
|
||||
return erroredRules;
|
||||
}
|
||||
|
||||
public Boolean shouldApplyRule(EntityInterface facts, SemanticsRule rule) {
|
||||
// If the rule is not entity-specific, apply it
|
||||
if (rule.getEntityType() == null && nullOrEmpty(rule.getIgnoredEntities())) {
|
||||
return true;
|
||||
}
|
||||
// Then, apply the rule only if type matches
|
||||
if (rule.getEntityType() != null) {
|
||||
return Entity.getEntityRepository(rule.getEntityType()).getEntityClass().isInstance(facts);
|
||||
}
|
||||
// Finally, check if the rule is not ignored for the entity type
|
||||
if (!nullOrEmpty(rule.getIgnoredEntities())) {
|
||||
List<? extends Class<? extends EntityInterface>> ignoredEntities =
|
||||
rule.getIgnoredEntities().stream()
|
||||
.map(Entity::getEntityRepository)
|
||||
.map(EntityRepository::getEntityClass)
|
||||
.toList();
|
||||
return !ignoredEntities.contains(facts.getClass());
|
||||
}
|
||||
return true; // Default case, apply the rule
|
||||
}
|
||||
|
||||
private List<SemanticsRule> getEnabledEntitySemantics() {
|
||||
return SettingsCache.getSetting(SettingsType.ENTITY_RULES_SETTINGS, EntityRulesSettings.class)
|
||||
.getEntitySemantics()
|
||||
@ -125,13 +144,4 @@ public class RuleEngine {
|
||||
throw new RuleValidationException(rule, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private DataContract getEntityDataContractSafely(EntityInterface entity) {
|
||||
try {
|
||||
return dataContractRepository.loadEntityDataContract(entity.getEntityReference());
|
||||
} catch (Exception e) {
|
||||
LOG.debug("Failed to load data contracts for entity {}: {}", entity.getId(), e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,16 @@
|
||||
"name": "Multiple Users or Single Team Ownership",
|
||||
"description": "Validates that an entity has either multiple owners or a single team as the owner.",
|
||||
"rule": "{\"multipleUsersOrSingleTeamOwnership\":{\"var\":\"owners\"}}",
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
"provider": "system"
|
||||
},
|
||||
{
|
||||
"name": "Multiple Domains are not allowed",
|
||||
"description": "By default, we only allow entities to be assigned to a single domain, except for Users and Teams.",
|
||||
"rule": "{\"<=\":[{\"length\":{\"var\":\"domains\"}},1]}",
|
||||
"enabled": true,
|
||||
"ignoredEntities": ["user", "team", "persona", "bot"],
|
||||
"provider": "system"
|
||||
},
|
||||
{
|
||||
"name": "Tables can only have a single Glossary Term",
|
||||
|
@ -138,6 +138,7 @@ import org.openmetadata.schema.api.teams.CreateTeam;
|
||||
import org.openmetadata.schema.api.teams.CreateTeam.TeamType;
|
||||
import org.openmetadata.schema.api.tests.CreateTestSuite;
|
||||
import org.openmetadata.schema.configuration.AssetCertificationSettings;
|
||||
import org.openmetadata.schema.configuration.EntityRulesSettings;
|
||||
import org.openmetadata.schema.dataInsight.DataInsightChart;
|
||||
import org.openmetadata.schema.dataInsight.type.KpiTarget;
|
||||
import org.openmetadata.schema.entities.docStore.Document;
|
||||
@ -294,6 +295,8 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
|
||||
protected static final RandomStringGenerator RANDOM_STRING_GENERATOR =
|
||||
new Builder().filteredBy(Character::isLetterOrDigit).build();
|
||||
|
||||
public static final String MULTI_DOMAIN_RULE = "Multiple Domains are not allowed";
|
||||
|
||||
public static Domain DOMAIN;
|
||||
public static Domain SUB_DOMAIN;
|
||||
public static DataProduct DOMAIN_DATA_PRODUCT;
|
||||
@ -672,6 +675,24 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
|
||||
public abstract void assertFieldChange(String fieldName, Object expected, Object actual)
|
||||
throws IOException;
|
||||
|
||||
public void toggleMultiDomainSupport(Boolean enable) {
|
||||
SystemRepository systemRepository = Entity.getSystemRepository();
|
||||
|
||||
Settings currentSettings =
|
||||
systemRepository.getConfigWithKey(SettingsType.ENTITY_RULES_SETTINGS.toString());
|
||||
EntityRulesSettings entityRulesSettings =
|
||||
(EntityRulesSettings) currentSettings.getConfigValue();
|
||||
entityRulesSettings
|
||||
.getEntitySemantics()
|
||||
.forEach(
|
||||
rule -> {
|
||||
if (MULTI_DOMAIN_RULE.equals(rule.getName())) {
|
||||
rule.setEnabled(enable);
|
||||
}
|
||||
});
|
||||
systemRepository.updateSetting(currentSettings);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Common entity tests for GET operations
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -526,6 +526,25 @@ public class DataContractResourceTest extends OpenMetadataApplicationTest {
|
||||
assertEquals(testCase1.getId(), updated.getQualityExpectations().get(0).getId());
|
||||
assertNotNull(updated.getTestSuite());
|
||||
|
||||
// GET the data contract and verify all fields are persisted correctly after first update
|
||||
DataContract retrievedAfterFirstUpdate =
|
||||
getDataContract(created.getId(), "semantics,qualityExpectations,testSuite,status,owners");
|
||||
assertEquals(ContractStatus.Active, retrievedAfterFirstUpdate.getStatus());
|
||||
assertEquals(created.getId(), retrievedAfterFirstUpdate.getId());
|
||||
assertNotNull(retrievedAfterFirstUpdate.getSemantics());
|
||||
assertEquals(1, retrievedAfterFirstUpdate.getSemantics().size());
|
||||
assertEquals("ID Field Exists", retrievedAfterFirstUpdate.getSemantics().get(0).getName());
|
||||
assertEquals(
|
||||
"Checks that the ID field exists",
|
||||
retrievedAfterFirstUpdate.getSemantics().get(0).getDescription());
|
||||
assertEquals(
|
||||
"{\"!!\": {\"var\": \"id\"}}", retrievedAfterFirstUpdate.getSemantics().get(0).getRule());
|
||||
assertNotNull(retrievedAfterFirstUpdate.getQualityExpectations());
|
||||
assertEquals(1, retrievedAfterFirstUpdate.getQualityExpectations().size());
|
||||
assertEquals(
|
||||
testCase1.getId(), retrievedAfterFirstUpdate.getQualityExpectations().get(0).getId());
|
||||
assertNotNull(retrievedAfterFirstUpdate.getTestSuite());
|
||||
|
||||
// Now update with additional semantics and quality expectations
|
||||
List<SemanticsRule> updatedSemantics =
|
||||
List.of(
|
||||
@ -560,6 +579,109 @@ public class DataContractResourceTest extends OpenMetadataApplicationTest {
|
||||
assertNotNull(finalUpdated.getQualityExpectations());
|
||||
assertEquals(2, finalUpdated.getQualityExpectations().size());
|
||||
assertNotNull(finalUpdated.getTestSuite());
|
||||
|
||||
// GET the data contract and verify all fields are persisted correctly after final update
|
||||
DataContract retrievedAfterFinalUpdate =
|
||||
getDataContract(created.getId(), "semantics,qualityExpectations,testSuite,status,owners");
|
||||
assertEquals(ContractStatus.Active, retrievedAfterFinalUpdate.getStatus());
|
||||
assertEquals(created.getId(), retrievedAfterFinalUpdate.getId());
|
||||
assertNotNull(retrievedAfterFinalUpdate.getSemantics());
|
||||
assertEquals(2, retrievedAfterFinalUpdate.getSemantics().size());
|
||||
assertEquals("ID Field Exists", retrievedAfterFinalUpdate.getSemantics().get(0).getName());
|
||||
assertEquals(
|
||||
"Checks that the ID field exists",
|
||||
retrievedAfterFinalUpdate.getSemantics().get(0).getDescription());
|
||||
assertEquals(
|
||||
"{\"!!\": {\"var\": \"id\"}}", retrievedAfterFinalUpdate.getSemantics().get(0).getRule());
|
||||
assertEquals("Name Field Exists", retrievedAfterFinalUpdate.getSemantics().get(1).getName());
|
||||
assertEquals(
|
||||
"Checks that the name field exists",
|
||||
retrievedAfterFinalUpdate.getSemantics().get(1).getDescription());
|
||||
assertEquals(
|
||||
"{\"!!\": {\"var\": \"name\"}}", retrievedAfterFinalUpdate.getSemantics().get(1).getRule());
|
||||
assertNotNull(retrievedAfterFinalUpdate.getQualityExpectations());
|
||||
assertEquals(2, retrievedAfterFinalUpdate.getQualityExpectations().size());
|
||||
assertEquals(
|
||||
testCase1.getId(), retrievedAfterFinalUpdate.getQualityExpectations().get(0).getId());
|
||||
assertEquals(
|
||||
testCase2.getId(), retrievedAfterFinalUpdate.getQualityExpectations().get(1).getId());
|
||||
assertNotNull(retrievedAfterFinalUpdate.getTestSuite());
|
||||
|
||||
// Now update with schema changes (add and remove columns)
|
||||
List<org.openmetadata.schema.type.Column> initialSchema =
|
||||
List.of(
|
||||
new org.openmetadata.schema.type.Column()
|
||||
.withName("id")
|
||||
.withDataType(ColumnDataType.BIGINT)
|
||||
.withDisplayName("ID")
|
||||
.withDescription("Primary key"),
|
||||
new org.openmetadata.schema.type.Column()
|
||||
.withName("name")
|
||||
.withDataType(ColumnDataType.VARCHAR)
|
||||
.withDisplayName("Name")
|
||||
.withDescription("Entity name"));
|
||||
|
||||
create.withSchema(initialSchema);
|
||||
DataContract schemaUpdated = updateDataContract(create);
|
||||
|
||||
// Verify schema was added
|
||||
assertNotNull(schemaUpdated.getSchema());
|
||||
assertEquals(2, schemaUpdated.getSchema().size());
|
||||
assertEquals("id", schemaUpdated.getSchema().get(0).getName());
|
||||
assertEquals("name", schemaUpdated.getSchema().get(1).getName());
|
||||
|
||||
// GET the data contract and verify schema is persisted
|
||||
DataContract retrievedWithSchema =
|
||||
getDataContract(created.getId(), "schema,semantics,qualityExpectations,testSuite,status");
|
||||
assertNotNull(retrievedWithSchema.getSchema());
|
||||
assertEquals(2, retrievedWithSchema.getSchema().size());
|
||||
assertEquals("id", retrievedWithSchema.getSchema().get(0).getName());
|
||||
assertEquals(ColumnDataType.BIGINT, retrievedWithSchema.getSchema().get(0).getDataType());
|
||||
assertEquals("name", retrievedWithSchema.getSchema().get(1).getName());
|
||||
assertEquals(ColumnDataType.VARCHAR, retrievedWithSchema.getSchema().get(1).getDataType());
|
||||
|
||||
// Now update schema: remove 'name' column and add 'email' column
|
||||
List<org.openmetadata.schema.type.Column> updatedSchema =
|
||||
List.of(
|
||||
new org.openmetadata.schema.type.Column()
|
||||
.withName("id")
|
||||
.withDataType(ColumnDataType.BIGINT)
|
||||
.withDisplayName("ID")
|
||||
.withDescription("Primary key"),
|
||||
new org.openmetadata.schema.type.Column()
|
||||
.withName("email")
|
||||
.withDataType(ColumnDataType.VARCHAR)
|
||||
.withDisplayName("Email")
|
||||
.withDescription("User email address"));
|
||||
|
||||
create.withSchema(updatedSchema);
|
||||
DataContract finalSchemaUpdated = updateDataContract(create);
|
||||
|
||||
// Verify schema changes
|
||||
assertNotNull(finalSchemaUpdated.getSchema());
|
||||
assertEquals(2, finalSchemaUpdated.getSchema().size());
|
||||
assertEquals("id", finalSchemaUpdated.getSchema().get(0).getName());
|
||||
assertEquals("email", finalSchemaUpdated.getSchema().get(1).getName());
|
||||
|
||||
// GET the final data contract and verify all schema changes are persisted
|
||||
DataContract finalRetrieved =
|
||||
getDataContract(created.getId(), "schema,semantics,qualityExpectations,testSuite,status");
|
||||
assertNotNull(finalRetrieved.getSchema());
|
||||
assertEquals(2, finalRetrieved.getSchema().size());
|
||||
assertEquals("id", finalRetrieved.getSchema().get(0).getName());
|
||||
assertEquals(ColumnDataType.BIGINT, finalRetrieved.getSchema().get(0).getDataType());
|
||||
assertEquals("Primary key", finalRetrieved.getSchema().get(0).getDescription());
|
||||
assertEquals("email", finalRetrieved.getSchema().get(1).getName());
|
||||
assertEquals(ColumnDataType.VARCHAR, finalRetrieved.getSchema().get(1).getDataType());
|
||||
assertEquals("User email address", finalRetrieved.getSchema().get(1).getDescription());
|
||||
|
||||
// Verify the other fields are still intact after schema changes
|
||||
assertEquals(ContractStatus.Active, finalRetrieved.getStatus());
|
||||
assertNotNull(finalRetrieved.getSemantics());
|
||||
assertEquals(2, finalRetrieved.getSemantics().size());
|
||||
assertNotNull(finalRetrieved.getQualityExpectations());
|
||||
assertEquals(2, finalRetrieved.getQualityExpectations().size());
|
||||
assertNotNull(finalRetrieved.getTestSuite());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -2547,4 +2669,25 @@ public class DataContractResourceTest extends OpenMetadataApplicationTest {
|
||||
dataContractRepository.setPipelineServiceClient(originalPipelineClient);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
void testGetDataContractByEntityWithoutContract(TestInfo test) throws IOException {
|
||||
// Create a table that will NOT have a data contract associated
|
||||
Table tableWithoutContract = createUniqueTable(test.getDisplayName());
|
||||
|
||||
HttpResponseException exception =
|
||||
assertThrows(
|
||||
HttpResponseException.class,
|
||||
() -> getDataContractByEntityId(tableWithoutContract.getId(), "table", "owners"));
|
||||
|
||||
// Should return 404 since no data contract exists for this table
|
||||
assertEquals(Status.NOT_FOUND.getStatusCode(), exception.getStatusCode());
|
||||
|
||||
// Verify the error message makes sense
|
||||
String errorMessage = exception.getMessage();
|
||||
assertTrue(
|
||||
errorMessage.contains("DataContract") || errorMessage.contains("not found"),
|
||||
"Error message should indicate that no data contract was found: " + errorMessage);
|
||||
}
|
||||
}
|
||||
|
@ -2568,6 +2568,7 @@ public class TableResourceTest extends EntityResourceTest<Table, CreateTable> {
|
||||
|
||||
@Test
|
||||
void test_multipleDomainInheritance(TestInfo test) throws IOException {
|
||||
toggleMultiDomainSupport(false); // Disable multi-domain support for this test
|
||||
// Test inheritance of multiple domains from databaseService > database > databaseSchema > table
|
||||
CreateDatabaseService createDbService =
|
||||
dbServiceTest
|
||||
@ -2606,6 +2607,8 @@ public class TableResourceTest extends EntityResourceTest<Table, CreateTable> {
|
||||
verifyDomainsInSearch(
|
||||
table.getEntityReference(),
|
||||
List.of(DOMAIN.getEntityReference(), DOMAIN1.getEntityReference()));
|
||||
|
||||
toggleMultiDomainSupport(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -45,6 +45,7 @@ public abstract class ServiceResourceTest<T extends EntityInterface, K extends C
|
||||
|
||||
@Test
|
||||
void test_listWithDomainFilter(TestInfo test) throws HttpResponseException {
|
||||
toggleMultiDomainSupport(false); // Disable multi-domain support for this test
|
||||
DomainResourceTest domainTest = new DomainResourceTest();
|
||||
String domain1 =
|
||||
domainTest
|
||||
@ -83,5 +84,7 @@ public abstract class ServiceResourceTest<T extends EntityInterface, K extends C
|
||||
assertEquals(3, list.size()); // appears in c1, c3 and c4
|
||||
assertTrue(list.stream().anyMatch(s -> s.getName().equals(s3.getName())));
|
||||
assertTrue(list.stream().anyMatch(s -> s.getName().equals(s4.getName())));
|
||||
|
||||
toggleMultiDomainSupport(true);
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
@ -377,4 +378,54 @@ public class RuleEngineTests extends OpenMetadataApplicationTest {
|
||||
assertNotNull(fixedTable);
|
||||
RuleEngine.getInstance().evaluate(fixedTable);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
void validateRuleApplicationLogic(TestInfo test) {
|
||||
Table table = getMockTable(test);
|
||||
|
||||
SemanticsRule ruleWithoutFilters =
|
||||
new SemanticsRule()
|
||||
.withName("Rule without filters")
|
||||
.withDescription("This rule has no filters applied.")
|
||||
.withRule("");
|
||||
|
||||
Assertions.assertTrue(RuleEngine.getInstance().shouldApplyRule(table, ruleWithoutFilters));
|
||||
|
||||
SemanticsRule ruleWithIgnoredEntities =
|
||||
new SemanticsRule()
|
||||
.withName("Rule without filters")
|
||||
.withDescription("This rule has no filters applied.")
|
||||
.withRule("")
|
||||
.withIgnoredEntities(List.of(Entity.DOMAIN, Entity.GLOSSARY_TERM));
|
||||
// Not ignored the table, should pass
|
||||
Assertions.assertTrue(RuleEngine.getInstance().shouldApplyRule(table, ruleWithIgnoredEntities));
|
||||
|
||||
SemanticsRule ruleWithTableEntity =
|
||||
new SemanticsRule()
|
||||
.withName("Rule without filters")
|
||||
.withDescription("This rule has no filters applied.")
|
||||
.withRule("")
|
||||
.withEntityType(Entity.TABLE);
|
||||
// Not ignored the table, should pass
|
||||
Assertions.assertTrue(RuleEngine.getInstance().shouldApplyRule(table, ruleWithTableEntity));
|
||||
|
||||
SemanticsRule ruleWithDomainEntity =
|
||||
new SemanticsRule()
|
||||
.withName("Rule without filters")
|
||||
.withDescription("This rule has no filters applied.")
|
||||
.withRule("")
|
||||
.withEntityType(Entity.DOMAIN);
|
||||
// Ignored the table, should not pass
|
||||
Assertions.assertFalse(RuleEngine.getInstance().shouldApplyRule(table, ruleWithDomainEntity));
|
||||
|
||||
SemanticsRule ruleWithIgnoredTable =
|
||||
new SemanticsRule()
|
||||
.withName("Rule without filters")
|
||||
.withDescription("This rule has no filters applied.")
|
||||
.withRule("")
|
||||
.withIgnoredEntities(List.of(Entity.TABLE));
|
||||
// Ignored the table, should not pass
|
||||
Assertions.assertFalse(RuleEngine.getInstance().shouldApplyRule(table, ruleWithIgnoredTable));
|
||||
}
|
||||
}
|
||||
|
@ -240,6 +240,18 @@
|
||||
"description": "Type of the entity to which this semantics rule applies.",
|
||||
"type": "string",
|
||||
"default": null
|
||||
},
|
||||
"ignoredEntities": {
|
||||
"title": "Ignored Entities",
|
||||
"description": "List of entities to ignore for this semantics rule.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"provider": {
|
||||
"$ref": "#/definitions/providerType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -0,0 +1,7 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.375" y="0.375" width="23.25" height="23.25" rx="11.625" fill="#FFFAEB"/>
|
||||
<rect x="0.375" y="0.375" width="23.25" height="23.25" rx="11.625" stroke="#FEDF89" stroke-width="0.75"/>
|
||||
<g opacity="0.9">
|
||||
<path d="M15.75 15.75L8.5625 8.5625M17.625 12C17.625 15.1066 15.1066 17.625 12 17.625C8.8934 17.625 6.375 15.1066 6.375 12C6.375 8.8934 8.8934 6.375 12 6.375C15.1066 6.375 17.625 8.8934 17.625 12Z" stroke="#B54708" stroke-width="1.13" stroke-linecap="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 553 B |
@ -0,0 +1,5 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.375" y="0.375" width="23.25" height="23.25" rx="11.625" fill="#FEF3F2"/>
|
||||
<rect x="0.375" y="0.375" width="23.25" height="23.25" rx="11.625" stroke="#FECDCA" stroke-width="0.75"/>
|
||||
<path d="M13.875 10.125L10.125 13.875M10.125 10.125L13.875 13.875M18.25 12C18.25 15.4518 15.4518 18.25 12 18.25C8.54822 18.25 5.75 15.4518 5.75 12C5.75 8.54822 8.54822 5.75 12 5.75C15.4518 5.75 18.25 8.54822 18.25 12Z" stroke="#B42318" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 576 B |
@ -0,0 +1,12 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0.375C18.4203 0.375 23.625 5.57969 23.625 12C23.625 18.4203 18.4203 23.625 12 23.625C5.57969 23.625 0.375 18.4203 0.375 12C0.375 5.57969 5.57969 0.375 12 0.375Z" fill="#ECFDF3"/>
|
||||
<path d="M12 0.375C18.4203 0.375 23.625 5.57969 23.625 12C23.625 18.4203 18.4203 23.625 12 23.625C5.57969 23.625 0.375 18.4203 0.375 12C0.375 5.57969 5.57969 0.375 12 0.375Z" stroke="#ABEFC6" stroke-width="0.75"/>
|
||||
<g clip-path="url(#clip0_18423_295747)">
|
||||
<path d="M16.25 11.5429V12.0029C16.2494 13.0811 15.9003 14.1302 15.2547 14.9938C14.6091 15.8573 13.7016 16.4891 12.6677 16.7948C11.6337 17.1005 10.5286 17.0638 9.51724 16.6902C8.50584 16.3165 7.64233 15.6259 7.05548 14.7214C6.46863 13.8169 6.1899 12.7469 6.26084 11.671C6.33178 10.5951 6.7486 9.57103 7.44914 8.7514C8.14968 7.93177 9.09639 7.36055 10.1481 7.12293C11.1998 6.88532 12.3001 6.99403 13.285 7.43286M16.25 8L11.25 13.005L9.75 11.505" stroke="#067647" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_18423_295747">
|
||||
<rect width="12" height="12" fill="white" transform="translate(5.25 6)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -12,7 +12,6 @@
|
||||
*/
|
||||
|
||||
import Icon, { PlusOutlined } from '@ant-design/icons';
|
||||
import { Utils as QbUtils } from '@react-awesome-query-builder/antd';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
@ -33,8 +32,8 @@ import { ReactComponent as AddPlaceHolderIcon } from '../../assets/svg/add-place
|
||||
import { ReactComponent as IconEdit } from '../../assets/svg/edit-new.svg';
|
||||
import { ReactComponent as IconDelete } from '../../assets/svg/ic-delete.svg';
|
||||
import { SIZE } from '../../enums/common.enum';
|
||||
import { SearchIndex } from '../../enums/search.enum';
|
||||
import {
|
||||
ProviderType,
|
||||
SemanticsRule,
|
||||
Settings,
|
||||
SettingType,
|
||||
@ -43,7 +42,6 @@ import {
|
||||
getSettingsConfigFromConfigType,
|
||||
updateSettingsConfig,
|
||||
} from '../../rest/settingConfigAPI';
|
||||
import { getTreeConfig } from '../../utils/AdvancedSearchUtils';
|
||||
import i18n, { t } from '../../utils/i18next/LocalUtil';
|
||||
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||
import QueryBuilderWidget from '../common/Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget';
|
||||
@ -218,6 +216,7 @@ export const AddEditSemanticsRuleModal: React.FC<{
|
||||
? t('label.edit-data-asset-rule')
|
||||
: t('label.add-data-asset-rule')
|
||||
}
|
||||
width={800}
|
||||
onCancel={onCancel}
|
||||
onOk={handleSave}>
|
||||
<SemanticsRuleForm
|
||||
@ -324,28 +323,6 @@ export const useSemanticsRuleList = ({
|
||||
setDeleteSemanticsRule(null);
|
||||
};
|
||||
|
||||
const config = getTreeConfig({
|
||||
searchIndex: SearchIndex.DATA_ASSET,
|
||||
searchOutputType: SearchOutputType.JSONLogic,
|
||||
isExplorePage: false,
|
||||
tierOptions: Promise.resolve([]),
|
||||
});
|
||||
const getHumanStringRule = useCallback(
|
||||
(semanticsRule: SemanticsRule) => {
|
||||
const logic = JSON.parse(semanticsRule.rule);
|
||||
|
||||
const tree = QbUtils.loadFromJsonLogic(logic, config);
|
||||
const humanString = tree ? QbUtils.queryString(tree, config) : '';
|
||||
|
||||
// remove all the .fullyQualifiedName, .name, .tagFQN from the humanString
|
||||
return humanString?.replaceAll(
|
||||
/\.fullyQualifiedName|\.name|\.tagFQN/g,
|
||||
''
|
||||
);
|
||||
},
|
||||
[config]
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('label.name'),
|
||||
@ -360,15 +337,6 @@ export const useSemanticsRuleList = ({
|
||||
<RichTextEditorPreviewerNew markdown={description} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('label.rule'),
|
||||
className: 'col-rule',
|
||||
render: (_: string, record: SemanticsRule) => (
|
||||
<RichTextEditorPreviewerNew
|
||||
markdown={getHumanStringRule(record) || ''}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('label.enabled'),
|
||||
dataIndex: 'enabled',
|
||||
@ -388,12 +356,14 @@ export const useSemanticsRuleList = ({
|
||||
<Space>
|
||||
<Button
|
||||
className="text-secondary p-0 remove-button-background-hover"
|
||||
disabled={record.provider === ProviderType.System}
|
||||
icon={<Icon component={IconEdit} />}
|
||||
type="text"
|
||||
onClick={() => handleEditSemanticsRule(record)}
|
||||
/>
|
||||
<Button
|
||||
className="text-secondary p-0 remove-button-background-hover"
|
||||
disabled={record.provider === ProviderType.System}
|
||||
icon={<Icon component={IconDelete} />}
|
||||
type="text"
|
||||
onClick={() => handleDelete(record)}
|
||||
|
@ -37,6 +37,7 @@ import { EntityType } from '../../../enums/entity.enum';
|
||||
import { DataContract } from '../../../generated/entity/data/dataContract';
|
||||
import { Table } from '../../../generated/entity/data/table';
|
||||
import { createContract, updateContract } from '../../../rest/contractAPI';
|
||||
import { getUpdatedContractDetails } from '../../../utils/DataContract/DataContractUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
|
||||
import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider';
|
||||
import SchemaEditor from '../../Database/SchemaEditor/SchemaEditor';
|
||||
@ -44,8 +45,6 @@ import { ContractDetailFormTab } from '../ContractDetailFormTab/ContractDetailFo
|
||||
import { ContractQualityFormTab } from '../ContractQualityFormTab/ContractQualityFormTab';
|
||||
import { ContractSchemaFormTab } from '../ContractSchemaFormTab/ContractScehmaFormTab';
|
||||
import { ContractSemanticFormTab } from '../ContractSemanticFormTab/ContractSemanticFormTab';
|
||||
|
||||
import { getUpdatedContractDetails } from '../../../utils/DataContract/DataContractUtils';
|
||||
import './add-data-contract.less';
|
||||
|
||||
export interface FormStepProps {
|
||||
@ -204,13 +203,13 @@ const AddDataContract: React.FC<{
|
||||
|
||||
const cardTitle = useMemo(() => {
|
||||
return (
|
||||
<div className="d-flex items-center justify-between">
|
||||
<div className="add-contract-card-header d-flex items-center justify-between">
|
||||
<div className="d-flex item-center justify-between flex-1">
|
||||
<div>
|
||||
<Typography.Title className="m-0" level={5}>
|
||||
<Typography.Text className="add-contract-card-title">
|
||||
{t('label.add-contract-detail-plural')}
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="m-0 text-sm" type="secondary">
|
||||
</Typography.Text>
|
||||
<Typography.Paragraph className="add-contract-card-description">
|
||||
{t('message.add-contract-detail-description')}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
@ -267,7 +266,11 @@ const AddDataContract: React.FC<{
|
||||
);
|
||||
}, [mode, items, handleTabChange, activeTab, yaml]);
|
||||
|
||||
return <Card title={cardTitle}>{cardContent}</Card>;
|
||||
return (
|
||||
<Card className="add-contract-card" title={cardTitle}>
|
||||
{cardContent}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddDataContract;
|
||||
|
@ -15,10 +15,9 @@
|
||||
|
||||
.ant-tabs.ant-tabs-left.contract-tabs {
|
||||
.ant-tabs-content-holder {
|
||||
background: #fff;
|
||||
border-radius: 0 8px 8px 8px;
|
||||
flex: 1;
|
||||
padding-left: @size-lg;
|
||||
padding: @size-lg;
|
||||
border-radius: 0 8px 8px 8px;
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
margin-top: 0;
|
||||
@ -39,6 +38,7 @@
|
||||
height: initial;
|
||||
border: none;
|
||||
padding: 0;
|
||||
padding-top: 20px;
|
||||
background: transparent;
|
||||
|
||||
.ant-tabs-nav-list {
|
||||
@ -95,15 +95,44 @@
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
background: @white;
|
||||
}
|
||||
|
||||
// Content area styling
|
||||
.ant-tabs-content {
|
||||
background: #fff;
|
||||
background: @white;
|
||||
min-height: 500px;
|
||||
border-radius: 0 8px 8px 8px;
|
||||
}
|
||||
|
||||
// Contract detail Forms
|
||||
.contract-detail-form-tab-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: @grey-900;
|
||||
}
|
||||
|
||||
.contract-detail-form-tab-description {
|
||||
font-size: 14px;
|
||||
color: @grey-600;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.contract-form-content-container {
|
||||
margin-top: 20px;
|
||||
background: @white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid @border-color-7;
|
||||
box-shadow: @button-box-shadow-default;
|
||||
}
|
||||
|
||||
// Table styling
|
||||
|
||||
.ant-table-tbody > tr.ant-table-row-selected > td {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// Additional styling for the contract form container
|
||||
@ -175,3 +204,23 @@
|
||||
height: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.add-contract-card {
|
||||
.add-contract-card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: @grey-900;
|
||||
}
|
||||
|
||||
.add-contract-card-description {
|
||||
font-size: 14px;
|
||||
color: @grey-600;
|
||||
font-weight: 400;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
> .ant-card-body {
|
||||
padding: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
|
@ -96,24 +96,26 @@ export const ContractDetailFormTab: React.FC<{
|
||||
return (
|
||||
<>
|
||||
<Card className="container bg-grey p-box">
|
||||
<div className="m-b-sm">
|
||||
<Typography.Title className="m-0" level={5}>
|
||||
<div>
|
||||
<Typography.Text className="contract-detail-form-tab-title">
|
||||
{t('label.contract-detail-plural')}
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="m-0 text-sm" type="secondary">
|
||||
</Typography.Text>
|
||||
<Typography.Paragraph className="contract-detail-form-tab-description">
|
||||
{t('message.contract-detail-plural-description')}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
className="bg-white p-box"
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={onNext}>
|
||||
{generateFormFields(fields)}
|
||||
<div className="contract-form-content-container">
|
||||
<Form
|
||||
className="contract-detail-form"
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={onNext}>
|
||||
{generateFormFields(fields)}
|
||||
|
||||
{owners?.length > 0 && <OwnerLabel owners={owners} />}
|
||||
</Form>
|
||||
{owners?.length > 0 && <OwnerLabel owners={owners} />}
|
||||
</Form>
|
||||
</div>
|
||||
</Card>
|
||||
<div className="d-flex justify-end m-t-md">
|
||||
<Button htmlType="submit" type="primary" onClick={handleSubmit}>
|
||||
|
@ -10,23 +10,23 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import Icon, {
|
||||
EditOutlined,
|
||||
PlayCircleOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import Icon, { PlayCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Loading } from '@melloware/react-logviewer';
|
||||
import { Button, Card, Col, Row, Space, Tag, Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg';
|
||||
import { ReactComponent as EmptyContractIcon } from '../../../assets/svg/empty-contract.svg';
|
||||
import { ReactComponent as FlagIcon } from '../../../assets/svg/flag.svg';
|
||||
import { ReactComponent as CheckIcon } from '../../../assets/svg/ic-check-circle.svg';
|
||||
import { ReactComponent as DeleteIcon } from '../../../assets/svg/ic-trash.svg';
|
||||
|
||||
import { Cell, Pie, PieChart } from 'recharts';
|
||||
import { NO_DATA_PLACEHOLDER } from '../../../constants/constants';
|
||||
import {
|
||||
ICON_DIMENSION,
|
||||
NO_DATA_PLACEHOLDER,
|
||||
} from '../../../constants/constants';
|
||||
import { DEFAULT_SORT_ORDER } from '../../../constants/profiler.constant';
|
||||
import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum';
|
||||
import { EntityType } from '../../../enums/entity.enum';
|
||||
@ -46,20 +46,20 @@ import {
|
||||
getContractStatusType,
|
||||
getTestCaseSummaryChartItems,
|
||||
} from '../../../utils/DataContract/DataContractUtils';
|
||||
import { getTestCaseStatusIcon } from '../../../utils/DataQuality/DataQualityUtils';
|
||||
import { getRelativeTime } from '../../../utils/date-time/DateTimeUtils';
|
||||
import { getEntityName } from '../../../utils/EntityUtils';
|
||||
import { pruneEmptyChildren } from '../../../utils/TableUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
|
||||
import DescriptionV1 from '../../common/EntityDescription/DescriptionV1';
|
||||
import ErrorPlaceHolderNew from '../../common/ErrorWithPlaceholder/ErrorPlaceHolderNew';
|
||||
import ExpandableCard from '../../common/ExpandableCard/ExpandableCard';
|
||||
import { OwnerAvatar } from '../../common/OwnerAvtar/OwnerAvatar';
|
||||
import { OwnerLabel } from '../../common/OwnerLabel/OwnerLabel.component';
|
||||
import { StatusType } from '../../common/StatusBadge/StatusBadge.interface';
|
||||
import StatusBadgeV2 from '../../common/StatusBadge/StatusBadgeV2.component';
|
||||
import Table from '../../common/Table/Table';
|
||||
import './contract-detail.less';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const ContractDetail: React.FC<{
|
||||
contract?: DataContract | null;
|
||||
onEdit: () => void;
|
||||
@ -129,7 +129,9 @@ const ContractDetail: React.FC<{
|
||||
title: t('label.name'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (name: string) => <Text className="text-primary">{name}</Text>,
|
||||
render: (name: string) => (
|
||||
<Typography.Text className="text-primary">{name}</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('label.type'),
|
||||
@ -216,19 +218,19 @@ const ContractDetail: React.FC<{
|
||||
return (
|
||||
<>
|
||||
{/* Header Section */}
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Card className="contract-header-container" style={{ marginBottom: 16 }}>
|
||||
<Row align="middle" justify="space-between">
|
||||
<Col flex="auto">
|
||||
<Title level={3} style={{ margin: 0 }}>
|
||||
{contract.displayName || contract.name}
|
||||
</Title>
|
||||
<Typography.Text className="contract-title">
|
||||
{getEntityName(contract)}
|
||||
</Typography.Text>
|
||||
|
||||
<Text type="secondary">
|
||||
<Typography.Text className="contract-time">
|
||||
{t('message.created-time-ago-by', {
|
||||
time: getRelativeTime(contract.updatedAt),
|
||||
by: contract.updatedBy,
|
||||
})}
|
||||
</Text>
|
||||
</Typography.Text>
|
||||
|
||||
<div className="d-flex items-center gap-2 m-t-xs">
|
||||
<StatusBadgeV2
|
||||
@ -236,19 +238,34 @@ const ContractDetail: React.FC<{
|
||||
label={t('label.active')}
|
||||
status={StatusType.Success}
|
||||
/>
|
||||
<Text type="secondary">
|
||||
{t('label.version-number', {
|
||||
|
||||
<StatusBadgeV2
|
||||
className="contract-version-badge"
|
||||
label={t('label.version-number', {
|
||||
version: contract.version,
|
||||
})}
|
||||
</Text>
|
||||
status={StatusType.Version}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<Space>
|
||||
<div className="contract-action-container">
|
||||
<div className="contract-owner-label-container">
|
||||
<Typography.Text>{t('label.owner-plural')}</Typography.Text>
|
||||
<OwnerLabel
|
||||
avatarSize={24}
|
||||
isCompactView={false}
|
||||
maxVisibleOwners={5}
|
||||
owners={contract.owners}
|
||||
showLabel={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="contract-run-now-button"
|
||||
icon={<PlayCircleOutlined />}
|
||||
loading={validateLoading}
|
||||
size="small"
|
||||
size="middle"
|
||||
onClick={handleRunNow}>
|
||||
{t('label.run-now')}
|
||||
</Button>
|
||||
@ -260,48 +277,61 @@ const ContractDetail: React.FC<{
|
||||
onClick={onDelete}
|
||||
/>
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
icon={
|
||||
<EditIcon className="anticon" style={{ ...ICON_DIMENSION }} />
|
||||
}
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={onEdit}>
|
||||
{t('label.edit')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
{contract.owners && contract.owners.length > 0 && (
|
||||
<Row style={{ marginTop: 16 }}>
|
||||
<Col>
|
||||
<Text strong style={{ marginRight: 8 }}>
|
||||
{t('label.owner')}
|
||||
</Text>
|
||||
<OwnerAvatar avatarSize={24} owner={contract.owners[0]} />
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Row className="contract-detail-container" gutter={[16, 0]}>
|
||||
{/* Left Column */}
|
||||
<Col span={12}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<DescriptionV1
|
||||
wrapInCard
|
||||
description={contract.description}
|
||||
entityType={EntityType.DATA_CONTRACT}
|
||||
showCommentsIcon={false}
|
||||
showSuggestions={false}
|
||||
/>
|
||||
<ExpandableCard
|
||||
cardProps={{
|
||||
className: 'expandable-card-contract',
|
||||
title: (
|
||||
<div className="contract-card-title-container">
|
||||
<Typography.Text className="contract-card-title">
|
||||
{t('label.entity-detail-plural', {
|
||||
entity: t('label.contract'),
|
||||
})}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="contract-card-description">
|
||||
{t('message.expected-schema-structure-of-this-asset')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
}}>
|
||||
<div className="expandable-card-contract-body">
|
||||
<DescriptionV1
|
||||
description={contract.description}
|
||||
entityType={EntityType.DATA_CONTRACT}
|
||||
showCommentsIcon={false}
|
||||
showSuggestions={false}
|
||||
/>
|
||||
</div>
|
||||
</ExpandableCard>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<ExpandableCard
|
||||
cardProps={{
|
||||
className: 'expandable-card-contract',
|
||||
title: (
|
||||
<div>
|
||||
<Title level={5}>{t('label.schema')}</Title>
|
||||
<Typography.Text type="secondary">
|
||||
<div className="contract-card-title-container">
|
||||
<Typography.Text className="contract-card-title">
|
||||
{t('label.schema')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="contract-card-description">
|
||||
{t('message.expected-schema-structure-of-this-asset')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
@ -328,10 +358,13 @@ const ContractDetail: React.FC<{
|
||||
<Col span={24}>
|
||||
<ExpandableCard
|
||||
cardProps={{
|
||||
className: 'expandable-card-contract',
|
||||
title: (
|
||||
<div>
|
||||
<Title level={5}>{t('label.contract-status')}</Title>
|
||||
<Typography.Text type="secondary">
|
||||
<div className="contract-card-title-container">
|
||||
<Typography.Text className="contract-card-title">
|
||||
{t('label.contract-status')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="contract-card-description">
|
||||
{t('message.contract-status-description')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
@ -352,16 +385,16 @@ const ContractDetail: React.FC<{
|
||||
/>
|
||||
|
||||
<div className="d-flex flex-column m-l-md">
|
||||
<Text className="contract-status-card-label">
|
||||
<Typography.Text className="contract-status-card-label">
|
||||
{item.label}
|
||||
</Text>
|
||||
</Typography.Text>
|
||||
<div>
|
||||
<Text className="contract-status-card-desc">
|
||||
<Typography.Text className="contract-status-card-desc">
|
||||
{item.desc}
|
||||
</Text>
|
||||
<Text className="contract-status-card-time">
|
||||
</Typography.Text>
|
||||
<Typography.Text className="contract-status-card-time">
|
||||
{item.time}
|
||||
</Text>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -382,27 +415,32 @@ const ContractDetail: React.FC<{
|
||||
<Col span={24}>
|
||||
<ExpandableCard
|
||||
cardProps={{
|
||||
className: 'expandable-card-contract',
|
||||
title: (
|
||||
<div>
|
||||
<Title level={5}>{t('label.semantic-plural')}</Title>
|
||||
<Typography.Text type="secondary">
|
||||
<div className="contract-card-title-container">
|
||||
<Typography.Text className="contract-card-title">
|
||||
{t('label.semantic-plural')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="contract-card-description">
|
||||
{t('message.semantics-description')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
}}>
|
||||
<Text className="card-subtitle">
|
||||
{t('label.custom-integrity-rules')}
|
||||
</Text>
|
||||
{(contract?.semantics ?? []).map((item) => (
|
||||
<div className="rule-item">
|
||||
<Icon className="rule-icon" component={CheckIcon} />
|
||||
<span className="rule-name">{item.name}</span>{' '}
|
||||
<span className="rule-description">
|
||||
{item.description}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="expandable-card-contract-body">
|
||||
<Typography.Text className="card-subtitle">
|
||||
{t('label.custom-integrity-rules')}
|
||||
</Typography.Text>
|
||||
{(contract?.semantics ?? []).map((item) => (
|
||||
<div className="rule-item">
|
||||
<Icon className="rule-icon" component={CheckIcon} />
|
||||
<span className="rule-name">{item.name}</span>{' '}
|
||||
<span className="rule-description">
|
||||
{item.description}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ExpandableCard>
|
||||
</Col>
|
||||
)}
|
||||
@ -412,93 +450,79 @@ const ContractDetail: React.FC<{
|
||||
<Col span={24}>
|
||||
<ExpandableCard
|
||||
cardProps={{
|
||||
className: 'expandable-card-contract',
|
||||
title: (
|
||||
<div>
|
||||
<Title level={5}>{t('label.quality')}</Title>
|
||||
<Typography.Text type="secondary">
|
||||
<div className="contract-card-title-container">
|
||||
<Typography.Text className="contract-card-title">
|
||||
{t('label.quality')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="contract-card-description">
|
||||
{t('message.data-quality-test-contract-title')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
),
|
||||
}}>
|
||||
{isTestCaseLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<Row
|
||||
className="data-quality-card-container"
|
||||
gutter={[0, 8]}>
|
||||
<Col span={24}>
|
||||
<Row
|
||||
align="middle"
|
||||
className="data-quality-chart-container"
|
||||
gutter={8}>
|
||||
<div className="expandable-card-contract-body">
|
||||
{isTestCaseLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<div className="data-quality-card-container">
|
||||
<div className="data-quality-chart-container">
|
||||
{testCaseSummaryChartItems.map((item) => (
|
||||
<Col key={item.label} span={6}>
|
||||
<div
|
||||
className="data-quality-chart-item"
|
||||
key={item.label}>
|
||||
<Text className="chart-label">
|
||||
{item.label}
|
||||
</Text>
|
||||
<div
|
||||
className="data-quality-chart-item"
|
||||
key={item.label}>
|
||||
<Typography.Text className="chart-label">
|
||||
{item.label}
|
||||
</Typography.Text>
|
||||
|
||||
<PieChart height={120} width={120}>
|
||||
<Pie
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
data={item.chartData}
|
||||
dataKey="value"
|
||||
innerRadius={40}
|
||||
outerRadius={50}>
|
||||
{item.chartData.map((entry, index) => (
|
||||
<Cell
|
||||
fill={entry.color}
|
||||
key={`cell-${index}`}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<text
|
||||
className="chart-center-text"
|
||||
dominantBaseline="middle"
|
||||
textAnchor="middle"
|
||||
x="50%"
|
||||
y="50%">
|
||||
{item.value}
|
||||
</text>
|
||||
</PieChart>
|
||||
</div>
|
||||
</Col>
|
||||
<PieChart height={120} width={120}>
|
||||
<Pie
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
data={item.chartData}
|
||||
dataKey="value"
|
||||
innerRadius={40}
|
||||
outerRadius={50}>
|
||||
{item.chartData.map((entry, index) => (
|
||||
<Cell
|
||||
fill={entry.color}
|
||||
key={`cell-${index}`}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<text
|
||||
className="chart-center-text"
|
||||
dominantBaseline="middle"
|
||||
textAnchor="middle"
|
||||
x="50%"
|
||||
y="50%">
|
||||
{item.value}
|
||||
</text>
|
||||
</PieChart>
|
||||
</div>
|
||||
))}
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={24} style={{ marginTop: 8 }}>
|
||||
</div>
|
||||
<Space direction="vertical">
|
||||
{testCaseResult.map((item) => (
|
||||
<div
|
||||
className="data-quality-item d-flex items-center"
|
||||
key={item.id}>
|
||||
<StatusBadgeV2
|
||||
className="data-quality-list-badge"
|
||||
label=""
|
||||
status={
|
||||
item.testCaseStatus
|
||||
? getContractStatusType(item.testCaseStatus)
|
||||
: StatusType.Pending
|
||||
}
|
||||
/>
|
||||
{getTestCaseStatusIcon(item)}
|
||||
<div className="data-quality-item-content">
|
||||
<Typography.Text className="data-quality-item-name">
|
||||
{item.name}
|
||||
</Typography.Text>
|
||||
<Text className="data-quality-item-description">
|
||||
<Typography.Text className="data-quality-item-description">
|
||||
{item.description}
|
||||
</Text>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ExpandableCard>
|
||||
</Col>
|
||||
)}
|
||||
|
@ -12,6 +12,108 @@
|
||||
*/
|
||||
@import (reference) url('../../../styles/variables.less');
|
||||
|
||||
.contract-header-container {
|
||||
.contract-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: @grey-900;
|
||||
}
|
||||
|
||||
.contract-time {
|
||||
font-size: 14px;
|
||||
color: @grey-600;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.contract-status-badge {
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.contract-action-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.contract-owner-label-container {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 16px;
|
||||
border-radius: 8px;
|
||||
border: @global-border;
|
||||
box-shadow: 0 2px 2px -1px @grey-27, 0 4px 6px -2px @grey-27;
|
||||
|
||||
.ant-typography {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: @grey-900;
|
||||
}
|
||||
|
||||
.owner-label-container {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.contract-run-now-button {
|
||||
color: @primary-color;
|
||||
|
||||
&:not(:hover) {
|
||||
border-color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contract-detail-container {
|
||||
.contract-card-title-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.contract-card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: @grey-900;
|
||||
}
|
||||
|
||||
.contract-card-description {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: @grey-600;
|
||||
}
|
||||
}
|
||||
|
||||
.schema-description {
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
.expandable-card-contract {
|
||||
background: @grey-50;
|
||||
|
||||
.ant-card-body {
|
||||
padding: 0 20px 20px 20px;
|
||||
|
||||
.expandable-card-contract-body {
|
||||
padding: 24px;
|
||||
background: @white;
|
||||
border-radius: @border-rad-sm;
|
||||
border: 1px solid @grey-9;
|
||||
box-shadow: 0 1px 2px 0 @grey-27;
|
||||
|
||||
.card-subtitle {
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: @grey-700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
background: transparent !important;
|
||||
border-color: @error-color !important;
|
||||
@ -54,14 +156,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: @grey-700;
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@ -86,13 +180,17 @@
|
||||
|
||||
.data-quality-card-container {
|
||||
.chart-label {
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: @grey-900;
|
||||
}
|
||||
|
||||
.data-quality-chart-container {
|
||||
border: 1px solid @border-color;
|
||||
margin-bottom: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 16px;
|
||||
border: 1px solid @border-color-8;
|
||||
border-radius: @card-radius;
|
||||
padding: @padding-md;
|
||||
box-shadow: @button-box-shadow-default;
|
||||
@ -131,4 +229,11 @@
|
||||
font-weight: 400;
|
||||
color: @grey-600;
|
||||
}
|
||||
|
||||
.test-status-icon {
|
||||
svg {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,11 +99,16 @@ export const ContractQualityFormTab: React.FC<{
|
||||
|
||||
return (
|
||||
<Card className="container bg-grey p-box">
|
||||
<Typography.Title level={5}>{t('label.quality')}</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
{t('message.quality-contract-description')}
|
||||
</Typography.Text>
|
||||
<Card>
|
||||
<div>
|
||||
<Typography.Text className="contract-detail-form-tab-title">
|
||||
{t('label.quality')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="contract-detail-form-tab-description">
|
||||
{t('message.quality-contract-description')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<div className="contract-form-content-container">
|
||||
<Radio.Group
|
||||
className="m-b-sm"
|
||||
value={testType}
|
||||
@ -125,7 +130,8 @@ export const ContractQualityFormTab: React.FC<{
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-between m-t-md">
|
||||
<Button icon={<ArrowLeftOutlined />} type="default" onClick={onPrev}>
|
||||
{prevLabel ?? t('label.previous')}
|
||||
|
@ -12,10 +12,12 @@
|
||||
*/
|
||||
import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NO_DATA_PLACEHOLDER } from '../../../constants/constants';
|
||||
import { TABLE_COLUMNS_KEYS } from '../../../constants/TableKeys.constants';
|
||||
import { EntityType } from '../../../enums/entity.enum';
|
||||
import { DataContract } from '../../../generated/entity/data/dataContract';
|
||||
import { Column } from '../../../generated/entity/data/table';
|
||||
@ -23,7 +25,10 @@ import { TagSource } from '../../../generated/tests/testCase';
|
||||
import { TagLabel } from '../../../generated/type/tagLabel';
|
||||
import { useFqn } from '../../../hooks/useFqn';
|
||||
import { getTableColumnsByFQN } from '../../../rest/tableAPI';
|
||||
import { highlightSearchArrayElement } from '../../../utils/EntityUtils';
|
||||
import {
|
||||
getEntityName,
|
||||
highlightSearchArrayElement,
|
||||
} from '../../../utils/EntityUtils';
|
||||
import { pruneEmptyChildren } from '../../../utils/TableUtils';
|
||||
import Table from '../../common/Table/Table';
|
||||
import { TableCellRendered } from '../../Database/SchemaTable/SchemaTable.interface';
|
||||
@ -75,22 +80,31 @@ export const ContractSchemaFormTab: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
const columns: ColumnsType<Column> = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: t('label.name'),
|
||||
dataIndex: 'name',
|
||||
dataIndex: TABLE_COLUMNS_KEYS.NAME,
|
||||
key: TABLE_COLUMNS_KEYS.NAME,
|
||||
render: (_, record: Column) => (
|
||||
<Typography.Text className="schema-table-name">
|
||||
{getEntityName(record)}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('label.type'),
|
||||
dataIndex: 'type',
|
||||
dataIndex: TABLE_COLUMNS_KEYS.DATA_TYPE_DISPLAY,
|
||||
key: TABLE_COLUMNS_KEYS.DATA_TYPE_DISPLAY,
|
||||
render: renderDataTypeDisplay,
|
||||
},
|
||||
{
|
||||
title: t('label.tag-plural'),
|
||||
dataIndex: 'tags',
|
||||
dataIndex: TABLE_COLUMNS_KEYS.TAGS,
|
||||
key: TABLE_COLUMNS_KEYS.TAGS,
|
||||
render: (tags: TagLabel[], record: Column, index: number) => (
|
||||
<TableTags<Column>
|
||||
isReadOnly
|
||||
entityFqn={fqn}
|
||||
entityType={EntityType.TABLE}
|
||||
handleTagSelection={() => Promise.resolve()}
|
||||
@ -104,9 +118,11 @@ export const ContractSchemaFormTab: React.FC<{
|
||||
},
|
||||
{
|
||||
title: t('label.glossary-term-plural'),
|
||||
dataIndex: 'glossaryTerms',
|
||||
dataIndex: TABLE_COLUMNS_KEYS.TAGS,
|
||||
key: TABLE_COLUMNS_KEYS.GLOSSARY,
|
||||
render: (tags: TagLabel[], record: Column, index: number) => (
|
||||
<TableTags<Column>
|
||||
isReadOnly
|
||||
entityFqn={fqn}
|
||||
entityType={EntityType.TABLE}
|
||||
handleTagSelection={() => Promise.resolve()}
|
||||
@ -120,7 +136,7 @@ export const ContractSchemaFormTab: React.FC<{
|
||||
},
|
||||
{
|
||||
title: t('label.constraint-plural'),
|
||||
dataIndex: 'contraints',
|
||||
dataIndex: 'constraint',
|
||||
},
|
||||
],
|
||||
[t]
|
||||
@ -130,10 +146,10 @@ export const ContractSchemaFormTab: React.FC<{
|
||||
<>
|
||||
<Card className="container bg-grey p-box">
|
||||
<div className="m-b-sm">
|
||||
<Typography.Title className="m-0" level={5}>
|
||||
<Typography.Text className="contract-detail-form-tab-title">
|
||||
{t('label.schema')}
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="m-0 text-sm" type="secondary">
|
||||
</Typography.Text>
|
||||
<Typography.Paragraph className="contract-detail-form-tab-description">
|
||||
{t('message.data-contract-schema-description')}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
|
@ -81,15 +81,13 @@ export const ContractSemanticFormTab: React.FC<{
|
||||
return (
|
||||
<>
|
||||
<Card className="container bg-grey p-box">
|
||||
<div className="d-flex justify-between items-center">
|
||||
<div>
|
||||
<Typography.Title level={5}>
|
||||
{t('label.semantic-plural')}
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
{t('message.semantics-description')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Text className="contract-detail-form-tab-title">
|
||||
{t('label.semantic-plural')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="contract-detail-form-tab-description">
|
||||
{t('message.semantics-description')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<Form form={form} layout="vertical">
|
||||
|
@ -11,7 +11,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AxiosError } from 'axios';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DataContractTabMode } from '../../../constants/DataContract.constants';
|
||||
@ -45,8 +44,8 @@ export const ContractTab = () => {
|
||||
TabSpecificField.OWNERS,
|
||||
]);
|
||||
setContract(contract);
|
||||
} catch (err) {
|
||||
showErrorToast(err as AxiosError);
|
||||
} catch {
|
||||
//
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ export enum StatusType {
|
||||
Pending = 'pending',
|
||||
InReview = 'inReview',
|
||||
Deprecated = 'deprecated',
|
||||
Version = 'version',
|
||||
}
|
||||
|
||||
export interface StatusBadgeProps {
|
||||
|
@ -70,6 +70,12 @@
|
||||
border-color: @red-17;
|
||||
}
|
||||
|
||||
&.version {
|
||||
color: @purple-5;
|
||||
background-color: @purple-1;
|
||||
border-color: @purple-5;
|
||||
}
|
||||
|
||||
svg {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { ReactComponent as SkippedIcon } from '../assets/svg/ic-aborted.svg';
|
||||
import { ReactComponent as FailedIcon } from '../assets/svg/ic-fail.svg';
|
||||
import { ReactComponent as SuccessIcon } from '../assets/svg/ic-successful.svg';
|
||||
|
||||
export const TEST_CASE_STATUS_ICON = {
|
||||
Aborted: SkippedIcon,
|
||||
Failed: FailedIcon,
|
||||
Queued: SkippedIcon,
|
||||
Success: SuccessIcon,
|
||||
};
|
@ -88,13 +88,13 @@ export const EntitySourceFields: Partial<Record<EntityFields, string[]>> = {
|
||||
// For example, in Glossary object, there are fields like name, description, parent, etc.
|
||||
export enum EntityReferenceFields {
|
||||
REVIEWERS = 'reviewers',
|
||||
OWNERS = 'owners.fullyQualifiedName',
|
||||
OWNERS = 'owners',
|
||||
DATABASE = 'database.name',
|
||||
DATABASE_SCHEMA = 'databaseSchema.name',
|
||||
DESCRIPTION = 'description',
|
||||
NAME = 'name',
|
||||
DISPLAY_NAME = 'displayName',
|
||||
TAG = 'tags.tagFQN',
|
||||
TAG = 'tags',
|
||||
TIER = 'tier.tagFQN',
|
||||
TABLE_TYPE = 'tableType',
|
||||
EXTENSION = 'extension',
|
||||
|
@ -599,16 +599,33 @@ export interface SemanticsRule {
|
||||
* Type of the entity to which this semantics rule applies.
|
||||
*/
|
||||
entityType?: string;
|
||||
/**
|
||||
* List of entities to ignore for this semantics rule.
|
||||
*/
|
||||
ignoredEntities?: string[];
|
||||
/**
|
||||
* Name of the semantics rule.
|
||||
*/
|
||||
name: string;
|
||||
name: string;
|
||||
provider?: ProviderType;
|
||||
/**
|
||||
* Definition of the semantics rule.
|
||||
*/
|
||||
rule: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of provider of an entity. Some entities are provided by the `system`. Some are
|
||||
* entities created and provided by the `user`. Typically `system` provide entities can't be
|
||||
* deleted and can only be disabled. Some apps such as AutoPilot create entities with
|
||||
* `automation` provider type. These entities can be deleted by the user.
|
||||
*/
|
||||
export enum ProviderType {
|
||||
Automation = "automation",
|
||||
System = "system",
|
||||
User = "user",
|
||||
}
|
||||
|
||||
/**
|
||||
* Status of the data contract.
|
||||
*/
|
||||
|
@ -33,12 +33,29 @@ export interface SemanticsRule {
|
||||
* Type of the entity to which this semantics rule applies.
|
||||
*/
|
||||
entityType?: string;
|
||||
/**
|
||||
* List of entities to ignore for this semantics rule.
|
||||
*/
|
||||
ignoredEntities?: string[];
|
||||
/**
|
||||
* Name of the semantics rule.
|
||||
*/
|
||||
name: string;
|
||||
name: string;
|
||||
provider?: ProviderType;
|
||||
/**
|
||||
* Definition of the semantics rule.
|
||||
*/
|
||||
rule: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of provider of an entity. Some entities are provided by the `system`. Some are
|
||||
* entities created and provided by the `user`. Typically `system` provide entities can't be
|
||||
* deleted and can only be disabled. Some apps such as AutoPilot create entities with
|
||||
* `automation` provider type. These entities can be deleted by the user.
|
||||
*/
|
||||
export enum ProviderType {
|
||||
Automation = "automation",
|
||||
System = "system",
|
||||
User = "user",
|
||||
}
|
||||
|
@ -761,16 +761,33 @@ export interface SemanticsRule {
|
||||
* Type of the entity to which this semantics rule applies.
|
||||
*/
|
||||
entityType?: string;
|
||||
/**
|
||||
* List of entities to ignore for this semantics rule.
|
||||
*/
|
||||
ignoredEntities?: string[];
|
||||
/**
|
||||
* Name of the semantics rule.
|
||||
*/
|
||||
name: string;
|
||||
name: string;
|
||||
provider?: ProviderType;
|
||||
/**
|
||||
* Definition of the semantics rule.
|
||||
*/
|
||||
rule: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of provider of an entity. Some entities are provided by the `system`. Some are
|
||||
* entities created and provided by the `user`. Typically `system` provide entities can't be
|
||||
* deleted and can only be disabled. Some apps such as AutoPilot create entities with
|
||||
* `automation` provider type. These entities can be deleted by the user.
|
||||
*/
|
||||
export enum ProviderType {
|
||||
Automation = "automation",
|
||||
System = "system",
|
||||
User = "user",
|
||||
}
|
||||
|
||||
/**
|
||||
* Status of the data contract.
|
||||
*/
|
||||
|
@ -892,16 +892,33 @@ export interface SemanticsRule {
|
||||
* Type of the entity to which this semantics rule applies.
|
||||
*/
|
||||
entityType?: string;
|
||||
/**
|
||||
* List of entities to ignore for this semantics rule.
|
||||
*/
|
||||
ignoredEntities?: string[];
|
||||
/**
|
||||
* Name of the semantics rule.
|
||||
*/
|
||||
name: string;
|
||||
name: string;
|
||||
provider?: ProviderType;
|
||||
/**
|
||||
* Definition of the semantics rule.
|
||||
*/
|
||||
rule: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of provider of an entity. Some entities are provided by the `system`. Some are
|
||||
* entities created and provided by the `user`. Typically `system` provide entities can't be
|
||||
* deleted and can only be disabled. Some apps such as AutoPilot create entities with
|
||||
* `automation` provider type. These entities can be deleted by the user.
|
||||
*/
|
||||
export enum ProviderType {
|
||||
Automation = "automation",
|
||||
System = "system",
|
||||
User = "user",
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to set up the Workflow Executor Settings.
|
||||
*/
|
||||
|
@ -976,3 +976,13 @@ a[href].link-text-grey,
|
||||
.transition-all-200ms {
|
||||
transition: all 200ms ease;
|
||||
}
|
||||
|
||||
// Test Case Status Icon
|
||||
|
||||
.test-status-icon {
|
||||
svg {
|
||||
fill: none;
|
||||
height: @size-lg;
|
||||
width: @size-lg;
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +89,7 @@
|
||||
@purple-2: #7147e8;
|
||||
@purple-3: #a2a1ff;
|
||||
@purple-4: #efedfe80;
|
||||
@purple-5: #6941c6;
|
||||
@blue-1: #ebf6fe;
|
||||
@blue-2: #3ca2f4;
|
||||
@blue-3: #0950c5;
|
||||
@ -201,6 +202,8 @@
|
||||
@border-color-4: #e4e4e7;
|
||||
@border-color-5: #dde3ea;
|
||||
@border-color-6: #dfdfdf;
|
||||
@border-color-7: #faf9fc;
|
||||
@border-color-8: #e9e8eb;
|
||||
@global-border: 1px solid @border-color;
|
||||
@nlp-border-color: #b9e6fe;
|
||||
@active-color: #e8f4ff;
|
||||
|
@ -10,6 +10,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import Icon from '@ant-design/icons';
|
||||
import { isArray, isNil, isUndefined, omit, omitBy } from 'lodash';
|
||||
import { ReactComponent as AccuracyIcon } from '../../assets/svg/ic-accuracy.svg';
|
||||
import { ReactComponent as CompletenessIcon } from '../../assets/svg/ic-completeness.svg';
|
||||
@ -20,10 +21,14 @@ import { ReactComponent as UniquenessIcon } from '../../assets/svg/ic-uniqueness
|
||||
import { ReactComponent as ValidityIcon } from '../../assets/svg/ic-validity.svg';
|
||||
import { ReactComponent as NoDimensionIcon } from '../../assets/svg/no-dimension-icon.svg';
|
||||
import { TestCaseSearchParams } from '../../components/DataQuality/DataQuality.interface';
|
||||
import { TEST_CASE_STATUS_ICON } from '../../constants/DataQuality.constants';
|
||||
import { TEST_CASE_FILTERS } from '../../constants/profiler.constant';
|
||||
import { Table } from '../../generated/entity/data/table';
|
||||
import { DataQualityReport } from '../../generated/tests/dataQualityReport';
|
||||
import { TestCaseParameterValue } from '../../generated/tests/testCase';
|
||||
import {
|
||||
TestCase,
|
||||
TestCaseParameterValue,
|
||||
} from '../../generated/tests/testCase';
|
||||
import {
|
||||
DataQualityDimensions,
|
||||
TestDataType,
|
||||
@ -322,3 +327,15 @@ export const convertSearchSourceToTable = (
|
||||
...searchSource,
|
||||
columns: searchSource.columns || [],
|
||||
} as Table);
|
||||
|
||||
export const getTestCaseStatusIcon = (record: TestCase) => (
|
||||
<Icon
|
||||
className="test-status-icon"
|
||||
component={
|
||||
TEST_CASE_STATUS_ICON[
|
||||
(record?.testCaseResult?.testCaseStatus ??
|
||||
'Queued') as keyof typeof TEST_CASE_STATUS_ICON
|
||||
]
|
||||
}
|
||||
/>
|
||||
);
|
@ -297,15 +297,23 @@ class JSONLogicSearchClassBase {
|
||||
},
|
||||
[EntityReferenceFields.OWNERS]: {
|
||||
label: t('label.owner-plural'),
|
||||
type: 'select',
|
||||
mainWidgetProps: this.mainWidgetProps,
|
||||
operators: this.defaultSelectOperators,
|
||||
fieldSettings: {
|
||||
asyncFetch: advancedSearchClassBase.autocomplete({
|
||||
searchIndex: [SearchIndex.USER, SearchIndex.TEAM],
|
||||
entityField: EntityFields.DISPLAY_NAME_KEYWORD,
|
||||
}),
|
||||
useAsyncSearch: true,
|
||||
type: '!group',
|
||||
mode: 'some',
|
||||
defaultField: 'fullyQualifiedName',
|
||||
subfields: {
|
||||
fullyQualifiedName: {
|
||||
label: 'Owners',
|
||||
type: 'select',
|
||||
mainWidgetProps: this.mainWidgetProps,
|
||||
operators: this.defaultSelectOperators,
|
||||
fieldSettings: {
|
||||
asyncFetch: advancedSearchClassBase.autocomplete({
|
||||
searchIndex: [SearchIndex.USER, SearchIndex.TEAM],
|
||||
entityField: EntityFields.DISPLAY_NAME_KEYWORD,
|
||||
}),
|
||||
useAsyncSearch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[EntityReferenceFields.DISPLAY_NAME]: {
|
||||
@ -347,19 +355,26 @@ class JSONLogicSearchClassBase {
|
||||
},
|
||||
[EntityReferenceFields.TAG]: {
|
||||
label: t('label.tag-plural'),
|
||||
type: 'select',
|
||||
mainWidgetProps: this.mainWidgetProps,
|
||||
operators: this.defaultSelectOperators,
|
||||
fieldSettings: {
|
||||
asyncFetch: this.searchAutocomplete({
|
||||
searchIndex: SearchIndex.TAG,
|
||||
fieldName: 'fullyQualifiedName',
|
||||
fieldLabel: 'name',
|
||||
}),
|
||||
useAsyncSearch: true,
|
||||
type: '!group',
|
||||
mode: 'some',
|
||||
defaultField: 'tagFQN',
|
||||
subfields: {
|
||||
tagFQN: {
|
||||
label: 'Tags',
|
||||
type: 'select',
|
||||
mainWidgetProps: this.mainWidgetProps,
|
||||
operators: this.defaultSelectOperators,
|
||||
fieldSettings: {
|
||||
asyncFetch: this.searchAutocomplete({
|
||||
searchIndex: SearchIndex.TAG,
|
||||
fieldName: 'fullyQualifiedName',
|
||||
fieldLabel: 'name',
|
||||
}),
|
||||
useAsyncSearch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
[EntityReferenceFields.EXTENSION]: {
|
||||
label: t('label.custom-property-plural'),
|
||||
type: '!struct',
|
||||
@ -424,7 +439,6 @@ class JSONLogicSearchClassBase {
|
||||
operatorLabel: t('label.condition') + ':',
|
||||
showNot: false,
|
||||
valueLabel: t('label.criteria') + ':',
|
||||
defaultField: EntityReferenceFields.OWNERS,
|
||||
renderButton: renderJSONLogicQueryBuilderButtons,
|
||||
customFieldSelectProps: {
|
||||
...this.baseConfig.settings.customFieldSelectProps,
|
||||
|
Loading…
x
Reference in New Issue
Block a user