diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java index c901640e3c7..b85675c0493 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/DataContractRepository.java @@ -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 { @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 addedColumns = new ArrayList<>(); + List deletedColumns = new ArrayList<>(); + recordListChange( + "schema", + original.getSchema(), + updated.getSchema(), + addedColumns, + deletedColumns, + EntityUtil.columnMatch); + } + + private void updateQualityExpectations(DataContract original, DataContract updated) { + List addedQualityExpectations = new ArrayList<>(); + List deletedQualityExpectations = new ArrayList<>(); + recordListChange( + "qualityExpectations", + original.getQualityExpectations(), + updated.getQualityExpectations(), + addedQualityExpectations, + deletedQualityExpectations, + EntityUtil.entityReferenceMatch); + } + + private void updateSemantics(DataContract original, DataContract updated) { + List addedSemantics = new ArrayList<>(); + List 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.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); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/data/DataContractResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/data/DataContractResource.java index ae0082370a0..7df76602224 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/data/DataContractResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/data/DataContractResource.java @@ -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 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 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> 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 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; - } - } } diff --git a/openmetadata-service/src/main/resources/json/data/settings/entityRulesSettings.json b/openmetadata-service/src/main/resources/json/data/settings/entityRulesSettings.json index fc57287a8cf..fdf9f5197e1 100644 --- a/openmetadata-service/src/main/resources/json/data/settings/entityRulesSettings.json +++ b/openmetadata-service/src/main/resources/json/data/settings/entityRulesSettings.json @@ -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", diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java index 0be116cb6ae..cf4df824bd6 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java @@ -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 { + if (MULTI_DOMAIN_RULE.equals(rule.getName())) { + rule.setEnabled(enable); + } + }); + systemRepository.updateSetting(currentSettings); + } + ////////////////////////////////////////////////////////////////////////////////////////////////// // Common entity tests for GET operations ////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/data/DataContractResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/data/DataContractResourceTest.java index 633470a87a3..5fcf629bd6b 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/data/DataContractResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/data/DataContractResourceTest.java @@ -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 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 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 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); + } } diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/databases/TableResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/databases/TableResourceTest.java index fb9a9ba4162..7aba79ac106 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/databases/TableResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/databases/TableResourceTest.java @@ -2568,6 +2568,7 @@ public class TableResourceTest extends EntityResourceTest { @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 { verifyDomainsInSearch( table.getEntityReference(), List.of(DOMAIN.getEntityReference(), DOMAIN1.getEntityReference())); + + toggleMultiDomainSupport(true); } @Test diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/services/ServiceResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/services/ServiceResourceTest.java index 2383a38f6cf..e2ed9cebc80 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/services/ServiceResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/services/ServiceResourceTest.java @@ -45,6 +45,7 @@ public abstract class ServiceResourceTest s.getName().equals(s3.getName()))); assertTrue(list.stream().anyMatch(s -> s.getName().equals(s4.getName()))); + + toggleMultiDomainSupport(true); } } diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/rules/RuleEngineTests.java b/openmetadata-service/src/test/java/org/openmetadata/service/rules/RuleEngineTests.java index d2acc317413..eed9b18bd2d 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/rules/RuleEngineTests.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/rules/RuleEngineTests.java @@ -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)); + } } diff --git a/openmetadata-spec/src/main/resources/json/schema/type/basic.json b/openmetadata-spec/src/main/resources/json/schema/type/basic.json index d8050ac1eea..18fc07dc2ac 100644 --- a/openmetadata-spec/src/main/resources/json/schema/type/basic.json +++ b/openmetadata-spec/src/main/resources/json/schema/type/basic.json @@ -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": [ diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-aborted.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-aborted.svg new file mode 100644 index 00000000000..c0f96533409 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-aborted.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-fail.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-fail.svg new file mode 100644 index 00000000000..262864cacd3 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-fail.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-successful.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-successful.svg new file mode 100644 index 00000000000..8d18bafe1c4 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-successful.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssetRules/DataAssetRules.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssetRules/DataAssetRules.component.tsx index 74d1d82317d..d90f8df4e8b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssetRules/DataAssetRules.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssetRules/DataAssetRules.component.tsx @@ -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}> { - 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 = ({ ), }, - { - title: t('label.rule'), - className: 'col-rule', - render: (_: string, record: SemanticsRule) => ( - - ), - }, { title: t('label.enabled'), dataIndex: 'enabled', @@ -388,12 +356,14 @@ export const useSemanticsRuleList = ({ @@ -260,48 +277,61 @@ const ContractDetail: React.FC<{ onClick={onDelete} /> - + - {contract.owners && contract.owners.length > 0 && ( - - - - {t('label.owner')} - - - - - )} - + {/* Left Column */} - + + + {t('label.entity-detail-plural', { + entity: t('label.contract'), + })} + + + {t('message.expected-schema-structure-of-this-asset')} + + + ), + }}> +
+ +
+
- {t('label.schema')} - +
+ + {t('label.schema')} + + {t('message.expected-schema-structure-of-this-asset')}
@@ -328,10 +358,13 @@ const ContractDetail: React.FC<{ - {t('label.contract-status')} - +
+ + {t('label.contract-status')} + + {t('message.contract-status-description')}
@@ -352,16 +385,16 @@ const ContractDetail: React.FC<{ />
- + {item.label} - +
- + {item.desc} - - + + {item.time} - +
@@ -382,27 +415,32 @@ const ContractDetail: React.FC<{ - {t('label.semantic-plural')} - +
+ + {t('label.semantic-plural')} + + {t('message.semantics-description')}
), }}> - - {t('label.custom-integrity-rules')} - - {(contract?.semantics ?? []).map((item) => ( -
- - {item.name}{' '} - - {item.description} - -
- ))} +
+ + {t('label.custom-integrity-rules')} + + {(contract?.semantics ?? []).map((item) => ( +
+ + {item.name}{' '} + + {item.description} + +
+ ))} +
)} @@ -412,93 +450,79 @@ const ContractDetail: React.FC<{ - {t('label.quality')} - +
+ + {t('label.quality')} + + {t('message.data-quality-test-contract-title')}
), }}> - {isTestCaseLoading ? ( - - ) : ( - - - +
+ {isTestCaseLoading ? ( + + ) : ( +
+
{testCaseSummaryChartItems.map((item) => ( - -
- - {item.label} - +
+ + {item.label} + - - - {item.chartData.map((entry, index) => ( - - ))} - - - {item.value} - - -
- + + + {item.chartData.map((entry, index) => ( + + ))} + + + {item.value} + + +
))} - - - +
{testCaseResult.map((item) => (
- + {getTestCaseStatusIcon(item)}
{item.name} - + {item.description} - +
))}
- - - )} +
+ )} +
)} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/contract-detail.less b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/contract-detail.less index 4c37a01862c..0918b9a0526 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/contract-detail.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractDetailTab/contract-detail.less @@ -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; + } + } } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractQualityFormTab/ContractQualityFormTab.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractQualityFormTab/ContractQualityFormTab.tsx index edad9a44dd6..d4e6737b622 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractQualityFormTab/ContractQualityFormTab.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataContract/ContractQualityFormTab/ContractQualityFormTab.tsx @@ -99,11 +99,16 @@ export const ContractQualityFormTab: React.FC<{ return ( - {t('label.quality')} - - {t('message.quality-contract-description')} - - +
+ + {t('label.quality')} + + + {t('message.quality-contract-description')} + +
+ +
- +
+