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:
Pere Miquel Brull 2025-08-01 14:29:56 +02:00 committed by GitHub
parent fe28faa13f
commit 30e2f83d50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 931 additions and 274 deletions

View File

@ -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);

View File

@ -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)));
}

View File

@ -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;
}
}
}

View File

@ -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",

View File

@ -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
//////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -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);
}
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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": [

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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}>

View File

@ -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>
)}

View File

@ -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;
}
}
}

View File

@ -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')}

View File

@ -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>

View File

@ -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">

View File

@ -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);
}

View File

@ -21,6 +21,7 @@ export enum StatusType {
Pending = 'pending',
InReview = 'inReview',
Deprecated = 'deprecated',
Version = 'version',
}
export interface StatusBadgeProps {

View File

@ -70,6 +70,12 @@
border-color: @red-17;
}
&.version {
color: @purple-5;
background-color: @purple-1;
border-color: @purple-5;
}
svg {
font-size: 12px;
}

View File

@ -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,
};

View File

@ -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',

View File

@ -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.
*/

View File

@ -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",
}

View File

@ -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.
*/

View File

@ -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.
*/

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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
]
}
/>
);

View File

@ -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,