From bdd9667ab7f8706fb0d4e9bdc077f00ef267fdd7 Mon Sep 17 00:00:00 2001 From: jayacryl <159848059+jayacryl@users.noreply.github.com> Date: Thu, 30 May 2024 11:08:31 -0400 Subject: [PATCH 01/26] feat(graphql) data contract resolvers for graphql --- .../datacontract/DataContractUtils.java | 32 ++ .../EntityDataContractResolver.java | 96 +++++ .../UpsertDataContractResolver.java | 278 +++++++++++++ .../datacontract/DataContractMapper.java | 112 ++++++ .../types/datacontract/DataContractType.java | 84 ++++ .../src/main/resources/contract.graphql | 183 +++++++++ .../src/main/resources/entity.graphql | 5 + .../datacontract/DataContractUtilsTest.java | 63 +++ .../EntityDataContractResolverTest.java | 206 ++++++++++ .../UpsertDataContractResolverTest.java | 379 ++++++++++++++++++ .../datacontract/DataContractMapperTest.java | 180 +++++++++ .../datacontract/DataContractTypeTest.java | 152 +++++++ .../java/com/linkedin/metadata/Constants.java | 6 + .../authorization/PoliciesConfig.java | 6 + 14 files changed, 1782 insertions(+) create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/datacontract/DataContractUtils.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/datacontract/EntityDataContractResolver.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/datacontract/UpsertDataContractResolver.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datacontract/DataContractMapper.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datacontract/DataContractType.java create mode 100644 datahub-graphql-core/src/main/resources/contract.graphql create mode 100644 datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/datacontract/DataContractUtilsTest.java create mode 100644 datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/datacontract/EntityDataContractResolverTest.java create mode 100644 datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/datacontract/UpsertDataContractResolverTest.java create mode 100644 datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/datacontract/DataContractMapperTest.java create mode 100644 datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/datacontract/DataContractTypeTest.java diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/datacontract/DataContractUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/datacontract/DataContractUtils.java new file mode 100644 index 00000000000000..3dd7cd9df63838 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/datacontract/DataContractUtils.java @@ -0,0 +1,32 @@ +package com.linkedin.datahub.graphql.resolvers.datacontract; + +import com.datahub.authorization.ConjunctivePrivilegeGroup; +import com.datahub.authorization.DisjunctivePrivilegeGroup; +import com.google.common.collect.ImmutableList; +import com.linkedin.common.urn.Urn; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; +import com.linkedin.metadata.authorization.PoliciesConfig; +import javax.annotation.Nonnull; + +public class DataContractUtils { + + public static boolean canEditDataContract(@Nonnull QueryContext context, Urn entityUrn) { + final DisjunctivePrivilegeGroup orPrivilegeGroups = + new DisjunctivePrivilegeGroup( + ImmutableList.of( + AuthorizationUtils.ALL_PRIVILEGES_GROUP, + new ConjunctivePrivilegeGroup( + ImmutableList.of( + PoliciesConfig.EDIT_ENTITY_DATA_CONTRACT_PRIVILEGE.getType())))); + + return AuthorizationUtils.isAuthorized( + context.getAuthorizer(), + context.getActorUrn(), + entityUrn.getEntityType(), + entityUrn.toString(), + orPrivilegeGroups); + } + + private DataContractUtils() {} +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/datacontract/EntityDataContractResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/datacontract/EntityDataContractResolver.java new file mode 100644 index 00000000000000..d742f4c021857f --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/datacontract/EntityDataContractResolver.java @@ -0,0 +1,96 @@ +package com.linkedin.datahub.graphql.resolvers.datacontract; + +import com.google.common.collect.ImmutableList; +import com.linkedin.common.EntityRelationship; +import com.linkedin.common.EntityRelationships; +import com.linkedin.common.urn.Urn; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.DataContract; +import com.linkedin.datahub.graphql.generated.Entity; +import com.linkedin.datahub.graphql.types.datacontract.DataContractMapper; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.graph.GraphClient; +import com.linkedin.metadata.query.filter.RelationshipDirection; +import com.linkedin.r2.RemoteInvocationException; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class EntityDataContractResolver implements DataFetcher> { + static final String CONTRACT_FOR_RELATIONSHIP = "ContractFor"; + + private final EntityClient _entityClient; + private final GraphClient _graphClient; + + public EntityDataContractResolver( + final EntityClient entityClient, final GraphClient graphClient) { + _entityClient = Objects.requireNonNull(entityClient, "entityClient must not be null"); + _graphClient = Objects.requireNonNull(graphClient, "graphClient must not be null"); + } + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) { + return CompletableFuture.supplyAsync( + () -> { + final QueryContext context = environment.getContext(); + final String entityUrn = ((Entity) environment.getSource()).getUrn(); + + try { + // Step 1: Fetch the contract associated with the dataset. + final EntityRelationships relationships = + _graphClient.getRelatedEntities( + entityUrn, + ImmutableList.of(CONTRACT_FOR_RELATIONSHIP), + RelationshipDirection.INCOMING, + 0, + 1, + context.getActorUrn()); + + // If we found multiple contracts for same entity, we have an invalid system state. Log + // a warning. + if (relationships.getTotal() > 1) { + // Someone created 2 contracts for the same entity. Currently we do not handle this in + // the UI. + log.warn( + String.format( + "Unexpectedly found multiple contracts (%s) for entity with urn %s! This may lead to inconsistent behavior.", + relationships.getRelationships(), entityUrn)); + } + + final List contractUrns = + relationships.getRelationships().stream() + .map(EntityRelationship::getEntity) + .collect(Collectors.toList()); + + if (contractUrns.size() >= 1) { + final Urn contractUrn = contractUrns.get(0); + + // Step 2: Hydrate the contract entities based on the urns from step 1 + final EntityResponse entityResponse = + _entityClient.getV2( + context.getOperationContext(), + Constants.DATA_CONTRACT_ENTITY_NAME, + contractUrn, + null); + + if (entityResponse != null) { + // Step 4: Package and return result + return DataContractMapper.mapContract(entityResponse); + } + } + // No contract found + return null; + } catch (URISyntaxException | RemoteInvocationException e) { + throw new RuntimeException("Failed to retrieve Assertion Run Events from GMS", e); + } + }); + } +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/datacontract/UpsertDataContractResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/datacontract/UpsertDataContractResolver.java new file mode 100644 index 00000000000000..955a4ed0ee6b2b --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/datacontract/UpsertDataContractResolver.java @@ -0,0 +1,278 @@ +package com.linkedin.datahub.graphql.resolvers.datacontract; + +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*; +import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.*; + +import com.datahub.authentication.Authentication; +import com.google.common.collect.ImmutableList; +import com.linkedin.common.EntityRelationships; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datacontract.DataContractProperties; +import com.linkedin.datacontract.DataContractState; +import com.linkedin.datacontract.DataContractStatus; +import com.linkedin.datacontract.DataQualityContract; +import com.linkedin.datacontract.DataQualityContractArray; +import com.linkedin.datacontract.FreshnessContract; +import com.linkedin.datacontract.FreshnessContractArray; +import com.linkedin.datacontract.SchemaContract; +import com.linkedin.datacontract.SchemaContractArray; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.exception.AuthorizationException; +import com.linkedin.datahub.graphql.exception.DataHubGraphQLErrorCode; +import com.linkedin.datahub.graphql.exception.DataHubGraphQLException; +import com.linkedin.datahub.graphql.generated.DataContract; +import com.linkedin.datahub.graphql.generated.DataQualityContractInput; +import com.linkedin.datahub.graphql.generated.FreshnessContractInput; +import com.linkedin.datahub.graphql.generated.SchemaContractInput; +import com.linkedin.datahub.graphql.generated.UpsertDataContractInput; +import com.linkedin.datahub.graphql.types.datacontract.DataContractMapper; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.graph.GraphClient; +import com.linkedin.metadata.key.DataContractKey; +import com.linkedin.metadata.query.filter.RelationshipDirection; +import com.linkedin.metadata.utils.EntityKeyUtils; +import com.linkedin.mxe.MetadataChangeProposal; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class UpsertDataContractResolver implements DataFetcher> { + + private static final DataContractState DEFAULT_CONTRACT_STATE = DataContractState.ACTIVE; + private static final String CONTRACT_RELATIONSHIP_TYPE = "ContractFor"; + private final EntityClient _entityClient; + private final GraphClient _graphClient; + + public UpsertDataContractResolver( + final EntityClient entityClient, final GraphClient graphClient) { + _entityClient = Objects.requireNonNull(entityClient, "entityClient cannot be null"); + _graphClient = Objects.requireNonNull(graphClient, "graphClient cannot be null"); + } + + @Override + public CompletableFuture get(final DataFetchingEnvironment environment) + throws Exception { + final QueryContext context = environment.getContext(); + final UpsertDataContractInput input = + bindArgument(environment.getArgument("input"), UpsertDataContractInput.class); + final Urn entityUrn = UrnUtils.getUrn(input.getEntityUrn()); + return CompletableFuture.supplyAsync( + () -> { + if (DataContractUtils.canEditDataContract(context, entityUrn)) { + + // Verify that the provided contract, dataset, assertions all exist as valid entities. + validateInput(entityUrn, input, context); + + // First determine if there is an existing data contract + final Urn maybeExistingContractUrn = + getEntityContractUrn(entityUrn, context.getAuthentication()); + + final DataContractProperties newProperties = mapInputToProperties(entityUrn, input); + final DataContractStatus newStatus = mapInputToStatus(input); + + final Urn urn = + maybeExistingContractUrn != null + ? maybeExistingContractUrn + : EntityKeyUtils.convertEntityKeyToUrn( + new DataContractKey() + .setId( + input.getId() != null + ? input.getId() + : UUID.randomUUID().toString()), + Constants.DATA_CONTRACT_ENTITY_NAME); + + final MetadataChangeProposal propertiesProposal = + buildMetadataChangeProposalWithUrn( + urn, Constants.DATA_CONTRACT_PROPERTIES_ASPECT_NAME, newProperties); + + final MetadataChangeProposal statusProposal = + buildMetadataChangeProposalWithUrn( + urn, Constants.DATA_CONTRACT_STATUS_ASPECT_NAME, newStatus); + + try { + _entityClient.batchIngestProposals( + context.getOperationContext(), + ImmutableList.of(propertiesProposal, statusProposal), + false); + + // Hydrate the contract entities based on the urns from step 1 + final EntityResponse entityResponse = + _entityClient.getV2( + context.getOperationContext(), + Constants.DATA_CONTRACT_ENTITY_NAME, + urn, + null); + + // Package and return result + return DataContractMapper.mapContract(entityResponse); + } catch (Exception e) { + throw new RuntimeException( + String.format("Failed to perform update against input %s", input.toString()), e); + } + } + throw new AuthorizationException( + "Unauthorized to perform this action. Please contact your DataHub administrator."); + }); + } + + private void validateInput( + @Nonnull final Urn entityUrn, + @Nonnull final UpsertDataContractInput input, + @Nonnull final QueryContext context) { + try { + + // Validate the target entity exists + if (!_entityClient.exists(context.getOperationContext(), entityUrn)) { + throw new DataHubGraphQLException( + String.format("Provided entity with urn %s does not exist!", entityUrn), + DataHubGraphQLErrorCode.BAD_REQUEST); + } + + // Verify Freshness assertions + if (input.getFreshness() != null) { + final List freshnessInputs = input.getFreshness(); + for (FreshnessContractInput freshnessInput : freshnessInputs) { + final Urn assertionUrn = UrnUtils.getUrn(freshnessInput.getAssertionUrn()); + if (!_entityClient.exists(context.getOperationContext(), assertionUrn)) { + throw new DataHubGraphQLException( + String.format("Provided assertion with urn %s does not exist!", assertionUrn), + DataHubGraphQLErrorCode.BAD_REQUEST); + } + } + } + + // Verify Schema assertions + if (input.getSchema() != null) { + final List schemaInputs = input.getSchema(); + for (SchemaContractInput schemaInput : schemaInputs) { + final Urn assertionUrn = UrnUtils.getUrn(schemaInput.getAssertionUrn()); + if (!_entityClient.exists(context.getOperationContext(), assertionUrn)) { + throw new DataHubGraphQLException( + String.format("Provided assertion with urn %s does not exist!", assertionUrn), + DataHubGraphQLErrorCode.BAD_REQUEST); + } + } + } + + // Verify DQ assertions + if (input.getDataQuality() != null) { + final List dqInputs = input.getDataQuality(); + for (DataQualityContractInput dqInput : dqInputs) { + final Urn assertionUrn = UrnUtils.getUrn(dqInput.getAssertionUrn()); + if (!_entityClient.exists(context.getOperationContext(), assertionUrn)) { + throw new DataHubGraphQLException( + String.format("Provided assertion with urn %s does not exist!", assertionUrn), + DataHubGraphQLErrorCode.BAD_REQUEST); + } + } + } + } catch (Exception e) { + if (e instanceof DataHubGraphQLException) { + throw (DataHubGraphQLException) e; + } else { + log.error( + "Failed to validate inputs provided when upserting data contract! Failing the create.", + e); + throw new DataHubGraphQLException( + "Failed to verify inputs. An unknown error occurred!", + DataHubGraphQLErrorCode.SERVER_ERROR); + } + } + } + + @Nullable + private Urn getEntityContractUrn(@Nonnull Urn entityUrn, @Nonnull Authentication authentication) { + EntityRelationships relationships = + _graphClient.getRelatedEntities( + entityUrn.toString(), + ImmutableList.of(CONTRACT_RELATIONSHIP_TYPE), + RelationshipDirection.INCOMING, + 0, + 1, + authentication.getActor().toUrnStr()); + + if (relationships.getTotal() > 1) { + // Bad state - There are multiple contracts for a single entity! Cannot update. + log.warn( + String.format( + "Unexpectedly found multiple contracts (%s) for entity with urn %s! This may lead to inconsistent behavior.", + relationships.getRelationships(), entityUrn)); + } + + if (relationships.getRelationships().size() == 1) { + return relationships.getRelationships().get(0).getEntity(); + } + // No Contract Found + return null; + } + + private DataContractProperties mapInputToProperties( + @Nonnull final Urn entityUrn, @Nonnull final UpsertDataContractInput input) { + final DataContractProperties result = new DataContractProperties(); + result.setEntity(entityUrn); + + // Construct the dataset contract. + if (input.getFreshness() != null) { + result.setFreshness( + new FreshnessContractArray( + input.getFreshness().stream() + .map(this::mapFreshnessInput) + .collect(Collectors.toList()))); + } + + if (input.getSchema() != null) { + result.setSchema( + new SchemaContractArray( + input.getSchema().stream().map(this::mapSchemaInput).collect(Collectors.toList()))); + } + + if (input.getDataQuality() != null) { + result.setDataQuality( + new DataQualityContractArray( + input.getDataQuality().stream() + .map(this::mapDataQualityInput) + .collect(Collectors.toList()))); + } + + return result; + } + + private DataContractStatus mapInputToStatus(@Nonnull final UpsertDataContractInput input) { + final DataContractStatus result = new DataContractStatus(); + if (input.getState() != null) { + result.setState(DataContractState.valueOf(input.getState().toString())); + } else { + result.setState(DEFAULT_CONTRACT_STATE); + } + return result; + } + + private FreshnessContract mapFreshnessInput(@Nonnull final FreshnessContractInput input) { + final FreshnessContract result = new FreshnessContract(); + result.setAssertion(UrnUtils.getUrn(input.getAssertionUrn())); + return result; + } + + private SchemaContract mapSchemaInput(@Nonnull final SchemaContractInput input) { + final SchemaContract result = new SchemaContract(); + result.setAssertion(UrnUtils.getUrn(input.getAssertionUrn())); + return result; + } + + private DataQualityContract mapDataQualityInput(@Nonnull final DataQualityContractInput input) { + final DataQualityContract result = new DataQualityContract(); + result.setAssertion(UrnUtils.getUrn(input.getAssertionUrn())); + return result; + } +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datacontract/DataContractMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datacontract/DataContractMapper.java new file mode 100644 index 00000000000000..1fe65beed6e927 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datacontract/DataContractMapper.java @@ -0,0 +1,112 @@ +package com.linkedin.datahub.graphql.types.datacontract; + +import com.linkedin.datahub.graphql.generated.Assertion; +import com.linkedin.datahub.graphql.generated.DataContract; +import com.linkedin.datahub.graphql.generated.DataContractProperties; +import com.linkedin.datahub.graphql.generated.DataContractState; +import com.linkedin.datahub.graphql.generated.DataContractStatus; +import com.linkedin.datahub.graphql.generated.DataQualityContract; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.generated.FreshnessContract; +import com.linkedin.datahub.graphql.generated.SchemaContract; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.EnvelopedAspect; +import com.linkedin.entity.EnvelopedAspectMap; +import com.linkedin.metadata.Constants; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; + +public class DataContractMapper { + + public static DataContract mapContract(@Nonnull final EntityResponse entityResponse) { + final DataContract result = new DataContract(); + final EnvelopedAspectMap aspects = entityResponse.getAspects(); + + result.setUrn(entityResponse.getUrn().toString()); + result.setType(EntityType.DATA_CONTRACT); + + final EnvelopedAspect dataContractProperties = + aspects.get(Constants.DATA_CONTRACT_PROPERTIES_ASPECT_NAME); + if (dataContractProperties != null) { + result.setProperties( + mapProperties( + new com.linkedin.datacontract.DataContractProperties( + dataContractProperties.getValue().data()))); + } else { + throw new RuntimeException( + String.format("Data Contract does not exist!. urn: %s", entityResponse.getUrn())); + } + + final EnvelopedAspect dataContractStatus = + aspects.get(Constants.DATA_CONTRACT_STATUS_ASPECT_NAME); + if (dataContractStatus != null) { + result.setStatus( + mapStatus( + new com.linkedin.datacontract.DataContractStatus( + dataContractStatus.getValue().data()))); + } + + return result; + } + + private static DataContractProperties mapProperties( + final com.linkedin.datacontract.DataContractProperties properties) { + final DataContractProperties result = new DataContractProperties(); + result.setEntityUrn(properties.getEntity().toString()); + if (properties.hasSchema()) { + result.setSchema( + properties.getSchema().stream() + .map(DataContractMapper::mapSchemaContract) + .collect(Collectors.toList())); + } + if (properties.hasFreshness()) { + result.setFreshness( + properties.getFreshness().stream() + .map(DataContractMapper::mapFreshnessContract) + .collect(Collectors.toList())); + } + if (properties.hasDataQuality()) { + result.setDataQuality( + properties.getDataQuality().stream() + .map(DataContractMapper::mapDataQualityContract) + .collect(Collectors.toList())); + } + return result; + } + + private static DataContractStatus mapStatus( + final com.linkedin.datacontract.DataContractStatus status) { + final DataContractStatus result = new DataContractStatus(); + result.setState(DataContractState.valueOf(status.getState().toString())); + return result; + } + + private static SchemaContract mapSchemaContract( + final com.linkedin.datacontract.SchemaContract schemaContract) { + final SchemaContract result = new SchemaContract(); + final Assertion partialAssertion = new Assertion(); + partialAssertion.setUrn(schemaContract.getAssertion().toString()); + result.setAssertion(partialAssertion); + return result; + } + + private static FreshnessContract mapFreshnessContract( + final com.linkedin.datacontract.FreshnessContract freshnessContract) { + final FreshnessContract result = new FreshnessContract(); + final Assertion partialAssertion = new Assertion(); + partialAssertion.setUrn(freshnessContract.getAssertion().toString()); + result.setAssertion(partialAssertion); + return result; + } + + private static DataQualityContract mapDataQualityContract( + final com.linkedin.datacontract.DataQualityContract qualityContract) { + final DataQualityContract result = new DataQualityContract(); + final Assertion partialAssertion = new Assertion(); + partialAssertion.setUrn(qualityContract.getAssertion().toString()); + result.setAssertion(partialAssertion); + return result; + } + + private DataContractMapper() {} +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datacontract/DataContractType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datacontract/DataContractType.java new file mode 100644 index 00000000000000..7f1756610baf71 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datacontract/DataContractType.java @@ -0,0 +1,84 @@ +package com.linkedin.datahub.graphql.types.datacontract; + +import com.google.common.collect.ImmutableSet; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.DataContract; +import com.linkedin.datahub.graphql.generated.Entity; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import graphql.execution.DataFetcherResult; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; + +public class DataContractType + implements com.linkedin.datahub.graphql.types.EntityType { + + static final Set ASPECTS_TO_FETCH = + ImmutableSet.of( + Constants.DATA_CONTRACT_KEY_ASPECT_NAME, + Constants.DATA_CONTRACT_PROPERTIES_ASPECT_NAME, + Constants.DATA_CONTRACT_STATUS_ASPECT_NAME); + private final EntityClient _entityClient; + + public DataContractType(final EntityClient entityClient) { + _entityClient = Objects.requireNonNull(entityClient, "entityClient must not be null"); + } + + @Override + public EntityType type() { + return EntityType.DATA_CONTRACT; + } + + @Override + public Function getKeyProvider() { + return Entity::getUrn; + } + + @Override + public Class objectClass() { + return DataContract.class; + } + + @Override + public List> batchLoad( + @Nonnull List urns, @Nonnull QueryContext context) throws Exception { + final List dataContractUrns = + urns.stream().map(UrnUtils::getUrn).collect(Collectors.toList()); + + try { + final Map entities = + _entityClient.batchGetV2( + context.getOperationContext(), + Constants.DATA_CONTRACT_ENTITY_NAME, + new HashSet<>(dataContractUrns), + ASPECTS_TO_FETCH); + + final List gmsResults = new ArrayList<>(); + for (Urn urn : dataContractUrns) { + gmsResults.add(entities.getOrDefault(urn, null)); + } + return gmsResults.stream() + .map( + gmsResult -> + gmsResult == null + ? null + : DataFetcherResult.newResult() + .data(DataContractMapper.mapContract(gmsResult)) + .build()) + .collect(Collectors.toList()); + } catch (Exception e) { + throw new RuntimeException("Failed to batch load Data Contracts", e); + } + } +} diff --git a/datahub-graphql-core/src/main/resources/contract.graphql b/datahub-graphql-core/src/main/resources/contract.graphql new file mode 100644 index 00000000000000..27d6510c89c24a --- /dev/null +++ b/datahub-graphql-core/src/main/resources/contract.graphql @@ -0,0 +1,183 @@ +extend type Mutation { + """ + Create or update a data contract for a given dataset. Requires the "Edit Data Contract" privilege for the provided dataset. + """ + upsertDataContract(urn: String, input: UpsertDataContractInput!): DataContract! +} + +extend type Dataset { + """ + An optional Data Contract defined for the Dataset. + """ + contract: DataContract +} + +""" +A Data Contract Entity. A Data Contract is a verifiable group of assertions regarding various aspects of the data: its freshness (sla), +schema, and data quality or validity. This group of assertions represents a data owner's commitment to producing data that confirms to the agreed +upon contract. Each dataset can have a single contract. The contract can be in a "passing" or "violating" state, depending +on whether the assertions that compose the contract are passing or failing. +Note that the data contract entity is currently in early preview (beta). +""" +type DataContract implements Entity { + """ + A primary key of the data contract + """ + urn: String! + + """ + The standard entity type + """ + type: EntityType! + + """ + Properties describing the data contract + """ + properties: DataContractProperties + + """ + The status of the data contract + """ + status: DataContractStatus + + """ + List of relationships between the source Entity and some destination entities with a given types + """ + relationships(input: RelationshipsInput!): EntityRelationshipsResult +} + +type DataContractProperties { + """ + The urn of the related entity, e.g. the Dataset today. In the future, we may support additional contract entities. + """ + entityUrn: String! + + """ + The Freshness (SLA) portion of the contract. + As of today, it is expected that there will not be more than 1 Freshness contract. If there are, only the first will be displayed. + """ + freshness: [FreshnessContract!] + + """ + The schema / structural portion of the contract. + As of today, it is expected that there will not be more than 1 Schema contract. If there are, only the first will be displayed. + """ + schema: [SchemaContract!] + + """ + A set of data quality related contracts, e.g. table and column-level contract constraints. + """ + dataQuality: [DataQualityContract!] +} + +""" +The state of the data contract +""" +enum DataContractState { + """ + The data contract is active. + """ + ACTIVE + + """ + The data contract is pending. Note that this symbol is currently experimental. + """ + PENDING +} + +type DataContractStatus { + """ + The state of the data contract + """ + state: DataContractState! +} + +type DataQualityContract { + """ + The assertion representing the schema contract. + """ + assertion: Assertion! +} + +type SchemaContract { + """ + The assertion representing the schema contract. + """ + assertion: Assertion! +} + +type FreshnessContract { + """ + The assertion representing the Freshness contract. + """ + assertion: Assertion! +} + +""" +Input required to upsert a Data Contract entity for an asset +""" +input UpsertDataContractInput { + """ + The urn of the related entity. Dataset is the only entity type supported today. + """ + entityUrn: String! + + """ + The Freshness / Freshness portion of the contract. If not provided, this will be set to none. + For Dataset Contracts, it is expected that there will not be more than 1 Freshness contract. If there are, only the first will be displayed. + """ + freshness: [FreshnessContractInput!] + + """ + The schema / structural portion of the contract. If not provided, this will be set to none. + For Dataset Contracts, it is expected that there will not be more than 1 Schema contract. If there are, only the first will be displayed. + """ + schema: [SchemaContractInput!] + + """ + The data quality portion of the contract. If not provided, this will be set to none. + """ + dataQuality: [DataQualityContractInput!] + + """ + The state of the data contract. If not provided, it will be in ACTIVE mode by default. + """ + state: DataContractState + + """ + Optional ID of the contract you want to create. Only applicable if this is a create operation. If not provided, a random + id will be generated for you. + """ + id: String +} + +""" +Input required to create an Freshness contract +""" +input FreshnessContractInput { + """ + The assertion monitoring this part of the data contract. Assertion must be of type Freshness. + """ + assertionUrn: String! +} + +""" +Input required to create a schema contract +""" +input SchemaContractInput { + """ + The assertion monitoring this part of the data contract. Assertion must be of type Data Schema. + """ + assertionUrn: String! +} + +""" +Input required to create a data quality contract +""" +input DataQualityContractInput { + """ + The assertion monitoring this part of the data contract. Assertion must be of type Dataset, Volume, Field / Column, or Custom SQL. + """ + assertionUrn: String! +} + diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index de030f77b0b017..38f51fa12956b7 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -1158,6 +1158,11 @@ enum EntityType { """ ROLE + """ + A data contract + """ + DATA_CONTRACT + """" An structured property on entities """ diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/datacontract/DataContractUtilsTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/datacontract/DataContractUtilsTest.java new file mode 100644 index 00000000000000..18ede7c306e424 --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/datacontract/DataContractUtilsTest.java @@ -0,0 +1,63 @@ +package com.linkedin.datahub.graphql.resolvers.datacontract; + +import static org.mockito.Mockito.mock; + +import com.datahub.authentication.Actor; +import com.datahub.authentication.ActorType; +import com.datahub.authentication.Authentication; +import com.datahub.authorization.AuthorizationRequest; +import com.datahub.authorization.AuthorizationResult; +import com.datahub.authorization.EntitySpec; +import com.datahub.plugins.auth.authorization.Authorizer; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datahub.graphql.QueryContext; +import graphql.Assert; +import io.datahubproject.metadata.context.OperationContext; +import io.datahubproject.test.metadata.context.TestOperationContexts; +import java.util.Optional; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +public class DataContractUtilsTest { + + @Test + public void testCanEditDataContract() { + Urn testUrn = UrnUtils.getUrn("urn:li:dataContract:test"); + boolean result = + DataContractUtils.canEditDataContract( + new QueryContext() { + @Override + public boolean isAuthenticated() { + return true; + } + + @Override + public Authentication getAuthentication() { + Authentication auth = new Authentication(new Actor(ActorType.USER, "test"), "TEST"); + return auth; + } + + @Override + public Authorizer getAuthorizer() { + Authorizer authorizer = mock(Authorizer.class); + Mockito.when(authorizer.authorize(Mockito.any(AuthorizationRequest.class))) + .thenReturn( + new AuthorizationResult( + new AuthorizationRequest( + "TEST", "test", Optional.of(new EntitySpec("dataset", "test"))), + AuthorizationResult.Type.ALLOW, + "TEST")); + return authorizer; + } + + @Override + public OperationContext getOperationContext() { + return TestOperationContexts.userContextNoSearchAuthorization( + getAuthorizer(), getAuthentication()); + } + }, + testUrn); + Assert.assertTrue(result); + } +} diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/datacontract/EntityDataContractResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/datacontract/EntityDataContractResolverTest.java new file mode 100644 index 00000000000000..8b757a24d6566f --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/datacontract/EntityDataContractResolverTest.java @@ -0,0 +1,206 @@ +package com.linkedin.datahub.graphql.resolvers.datacontract; + +import static com.linkedin.datahub.graphql.resolvers.datacontract.EntityDataContractResolver.*; +import static org.mockito.ArgumentMatchers.nullable; +import static org.testng.Assert.*; + +import com.datahub.authentication.Authentication; +import com.google.common.collect.ImmutableList; +import com.linkedin.common.AuditStamp; +import com.linkedin.common.EntityRelationship; +import com.linkedin.common.EntityRelationshipArray; +import com.linkedin.common.EntityRelationships; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datacontract.DataContractProperties; +import com.linkedin.datacontract.DataContractState; +import com.linkedin.datacontract.DataContractStatus; +import com.linkedin.datacontract.DataQualityContract; +import com.linkedin.datacontract.DataQualityContractArray; +import com.linkedin.datacontract.FreshnessContract; +import com.linkedin.datacontract.FreshnessContractArray; +import com.linkedin.datacontract.SchemaContract; +import com.linkedin.datacontract.SchemaContractArray; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.DataContract; +import com.linkedin.datahub.graphql.generated.Dataset; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.entity.Aspect; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.EnvelopedAspect; +import com.linkedin.entity.EnvelopedAspectMap; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.graph.GraphClient; +import com.linkedin.metadata.key.DataContractKey; +import com.linkedin.metadata.query.filter.RelationshipDirection; +import graphql.schema.DataFetchingEnvironment; +import io.datahubproject.metadata.context.OperationContext; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +public class EntityDataContractResolverTest { + + private static final Urn TEST_DATASET_URN = + UrnUtils.getUrn("urn:li:dataset:(urn:li:dataPlatform:snowflake,test,PROD)"); + private static final Urn TEST_DATA_CONTRACT_URN = UrnUtils.getUrn("urn:li:dataContract:test"); + private static final Urn TEST_QUALITY_ASSERTION_URN = UrnUtils.getUrn("urn:li:assertion:quality"); + private static final Urn TEST_FRESHNESS_ASSERTION_URN = + UrnUtils.getUrn("urn:li:assertion:freshness"); + private static final Urn TEST_SCHEMA_ASSERTION_URN = UrnUtils.getUrn("urn:li:assertion:schema"); + + @Test + public void testGetSuccessOneContract() throws Exception { + GraphClient mockGraphClient = Mockito.mock(GraphClient.class); + EntityClient mockClient = Mockito.mock(EntityClient.class); + + Mockito.when( + mockGraphClient.getRelatedEntities( + Mockito.eq(TEST_DATASET_URN.toString()), + Mockito.eq(ImmutableList.of(CONTRACT_FOR_RELATIONSHIP)), + Mockito.eq(RelationshipDirection.INCOMING), + Mockito.eq(0), + Mockito.eq(1), + Mockito.anyString())) + .thenReturn( + new EntityRelationships() + .setTotal(1) + .setCount(1) + .setStart(0) + .setRelationships( + new EntityRelationshipArray( + ImmutableList.of( + new EntityRelationship() + .setType(CONTRACT_FOR_RELATIONSHIP) + .setEntity(TEST_DATA_CONTRACT_URN) + .setCreated( + new AuditStamp() + .setActor(UrnUtils.getUrn("urn:li:corpuser:test")) + .setTime(0L)))))); + + Map dataContractAspects = new HashMap<>(); + + // 1. Key Aspect + dataContractAspects.put( + Constants.DATA_CONTRACT_KEY_ASPECT_NAME, + new com.linkedin.entity.EnvelopedAspect() + .setValue(new Aspect(new DataContractKey().setId("test").data()))); + + // 2. Properties Aspect. + DataContractProperties expectedProperties = + new DataContractProperties() + .setEntity(TEST_DATASET_URN) + .setDataQuality( + new DataQualityContractArray( + ImmutableList.of( + new DataQualityContract().setAssertion(TEST_QUALITY_ASSERTION_URN)))) + .setFreshness( + new FreshnessContractArray( + ImmutableList.of( + new FreshnessContract().setAssertion(TEST_FRESHNESS_ASSERTION_URN)))) + .setSchema( + new SchemaContractArray( + ImmutableList.of( + new SchemaContract().setAssertion(TEST_SCHEMA_ASSERTION_URN)))); + + dataContractAspects.put( + Constants.DATA_CONTRACT_PROPERTIES_ASPECT_NAME, + new com.linkedin.entity.EnvelopedAspect().setValue(new Aspect(expectedProperties.data()))); + + // 3. Status Aspect + DataContractStatus expectedStatus = new DataContractStatus().setState(DataContractState.ACTIVE); + + dataContractAspects.put( + Constants.DATA_CONTRACT_STATUS_ASPECT_NAME, + new com.linkedin.entity.EnvelopedAspect().setValue(new Aspect(expectedStatus.data()))); + + Mockito.when( + mockClient.getV2( + nullable(OperationContext.class), + Mockito.eq(Constants.DATA_CONTRACT_ENTITY_NAME), + Mockito.eq(TEST_DATA_CONTRACT_URN), + Mockito.eq(null))) + .thenReturn( + new EntityResponse() + .setEntityName(Constants.DATA_CONTRACT_ENTITY_NAME) + .setUrn(TEST_DATA_CONTRACT_URN) + .setAspects(new EnvelopedAspectMap(dataContractAspects))); + + // Execute resolver + QueryContext mockContext = Mockito.mock(QueryContext.class); + Mockito.when(mockContext.getAuthentication()).thenReturn(Mockito.mock(Authentication.class)); + Mockito.when(mockContext.getActorUrn()).thenReturn("urn:li:corpuser:test"); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Dataset parentDataset = new Dataset(); + parentDataset.setUrn(TEST_DATASET_URN.toString()); + Mockito.when(mockEnv.getSource()).thenReturn(parentDataset); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + EntityDataContractResolver resolver = + new EntityDataContractResolver(mockClient, mockGraphClient); + DataContract result = resolver.get(mockEnv).get(); + + // Assert that the result we get matches the expectations. + assertEquals(result.getUrn(), TEST_DATA_CONTRACT_URN.toString()); + assertEquals(result.getType(), EntityType.DATA_CONTRACT); + + // Verify Properties + assertEquals(result.getProperties().getDataQuality().size(), 1); + assertEquals(result.getProperties().getFreshness().size(), 1); + assertEquals(result.getProperties().getSchema().size(), 1); + assertEquals( + result.getProperties().getDataQuality().get(0).getAssertion().getUrn(), + TEST_QUALITY_ASSERTION_URN.toString()); + assertEquals( + result.getProperties().getFreshness().get(0).getAssertion().getUrn(), + TEST_FRESHNESS_ASSERTION_URN.toString()); + assertEquals( + result.getProperties().getSchema().get(0).getAssertion().getUrn(), + TEST_SCHEMA_ASSERTION_URN.toString()); + + // Verify Status + assertEquals(result.getStatus().getState().toString(), expectedStatus.getState().toString()); + } + + @Test + public void testGetSuccessNoContracts() throws Exception { + GraphClient mockGraphClient = Mockito.mock(GraphClient.class); + EntityClient mockClient = Mockito.mock(EntityClient.class); + + Mockito.when( + mockGraphClient.getRelatedEntities( + Mockito.eq(TEST_DATASET_URN.toString()), + Mockito.eq(ImmutableList.of(CONTRACT_FOR_RELATIONSHIP)), + Mockito.eq(RelationshipDirection.INCOMING), + Mockito.eq(0), + Mockito.eq(1), + Mockito.anyString())) + .thenReturn( + new EntityRelationships() + .setTotal(0) + .setCount(0) + .setStart(0) + .setRelationships(new EntityRelationshipArray(Collections.emptyList()))); + + EntityDataContractResolver resolver = + new EntityDataContractResolver(mockClient, mockGraphClient); + + // Execute resolver + QueryContext mockContext = Mockito.mock(QueryContext.class); + Mockito.when(mockContext.getAuthentication()).thenReturn(Mockito.mock(Authentication.class)); + Mockito.when(mockContext.getActorUrn()).thenReturn("urn:li:corpuser:test"); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Dataset parentDataset = new Dataset(); + parentDataset.setUrn(TEST_DATASET_URN.toString()); + Mockito.when(mockEnv.getSource()).thenReturn(parentDataset); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + DataContract result = resolver.get(mockEnv).get(); + + assertNull(result); + Mockito.verifyNoMoreInteractions(mockClient); + } +} diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/datacontract/UpsertDataContractResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/datacontract/UpsertDataContractResolverTest.java new file mode 100644 index 00000000000000..601fc56b251495 --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/datacontract/UpsertDataContractResolverTest.java @@ -0,0 +1,379 @@ +package com.linkedin.datahub.graphql.resolvers.datacontract; + +import static com.linkedin.datahub.graphql.TestUtils.*; +import static com.linkedin.datahub.graphql.resolvers.datacontract.EntityDataContractResolver.*; +import static org.mockito.ArgumentMatchers.any; +import static org.testng.Assert.*; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.linkedin.common.AuditStamp; +import com.linkedin.common.EntityRelationship; +import com.linkedin.common.EntityRelationshipArray; +import com.linkedin.common.EntityRelationships; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.data.template.StringMap; +import com.linkedin.datacontract.DataContractProperties; +import com.linkedin.datacontract.DataContractStatus; +import com.linkedin.datacontract.DataQualityContract; +import com.linkedin.datacontract.DataQualityContractArray; +import com.linkedin.datacontract.FreshnessContract; +import com.linkedin.datacontract.FreshnessContractArray; +import com.linkedin.datacontract.SchemaContract; +import com.linkedin.datacontract.SchemaContractArray; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.DataContract; +import com.linkedin.datahub.graphql.generated.DataContractState; +import com.linkedin.datahub.graphql.generated.DataQualityContractInput; +import com.linkedin.datahub.graphql.generated.FreshnessContractInput; +import com.linkedin.datahub.graphql.generated.SchemaContractInput; +import com.linkedin.datahub.graphql.generated.UpsertDataContractInput; +import com.linkedin.entity.Aspect; +import com.linkedin.entity.AspectType; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.EnvelopedAspect; +import com.linkedin.entity.EnvelopedAspectMap; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.events.metadata.ChangeType; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.graph.GraphClient; +import com.linkedin.metadata.key.DataContractKey; +import com.linkedin.metadata.query.filter.RelationshipDirection; +import com.linkedin.metadata.utils.EntityKeyUtils; +import com.linkedin.metadata.utils.GenericRecordUtils; +import com.linkedin.mxe.MetadataChangeProposal; +import com.linkedin.mxe.SystemMetadata; +import com.linkedin.r2.RemoteInvocationException; +import graphql.schema.DataFetchingEnvironment; +import io.datahubproject.metadata.context.OperationContext; +import java.util.Collections; +import java.util.concurrent.CompletionException; +import org.mockito.Mockito; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class UpsertDataContractResolverTest { + + private static final Urn TEST_CONTRACT_URN = UrnUtils.getUrn("urn:li:dataContract:test-id"); + private static final Urn TEST_DATASET_URN = + UrnUtils.getUrn("urn:li:dataset:(urn:li:dataPlatform:snowflake,test,PROD)"); + private static final Urn TEST_FRESHNESS_ASSERTION_URN = + UrnUtils.getUrn("urn:li:assertion:freshness"); + private static final Urn TEST_SCHEMA_ASSERTION_URN = UrnUtils.getUrn("urn:li:assertion:schema"); + private static final Urn TEST_QUALITY_ASSERTION_URN = UrnUtils.getUrn("urn:li:assertion:quality"); + + private static final UpsertDataContractInput TEST_CREATE_INPUT = + new UpsertDataContractInput( + TEST_DATASET_URN.toString(), + ImmutableList.of(new FreshnessContractInput(TEST_FRESHNESS_ASSERTION_URN.toString())), + ImmutableList.of(new SchemaContractInput(TEST_SCHEMA_ASSERTION_URN.toString())), + ImmutableList.of(new DataQualityContractInput(TEST_QUALITY_ASSERTION_URN.toString())), + DataContractState.PENDING, + "test-id"); + + private static final UpsertDataContractInput TEST_VALID_UPDATE_INPUT = + new UpsertDataContractInput( + TEST_DATASET_URN.toString(), + ImmutableList.of(new FreshnessContractInput(TEST_FRESHNESS_ASSERTION_URN.toString())), + ImmutableList.of(new SchemaContractInput(TEST_SCHEMA_ASSERTION_URN.toString())), + ImmutableList.of(new DataQualityContractInput(TEST_QUALITY_ASSERTION_URN.toString())), + DataContractState.ACTIVE, + null); + + private static final Urn TEST_ACTOR_URN = UrnUtils.getUrn("urn:li:corpuser:test"); + + @Test + public void testGetSuccessCreate() throws Exception { + + // Expected results + final DataContractKey key = new DataContractKey(); + key.setId("test-id"); + final Urn dataContractUrn = + EntityKeyUtils.convertEntityKeyToUrn(key, Constants.DATA_CONTRACT_ENTITY_NAME); + + final DataContractStatus status = new DataContractStatus(); + status.setState(com.linkedin.datacontract.DataContractState.PENDING); + + final DataContractProperties props = new DataContractProperties(); + props.setEntity(TEST_DATASET_URN); + props.setDataQuality( + new DataQualityContractArray( + ImmutableList.of(new DataQualityContract().setAssertion(TEST_QUALITY_ASSERTION_URN)))); + props.setFreshness( + new FreshnessContractArray( + ImmutableList.of(new FreshnessContract().setAssertion(TEST_FRESHNESS_ASSERTION_URN)))); + props.setSchema( + new SchemaContractArray( + ImmutableList.of(new SchemaContract().setAssertion(TEST_SCHEMA_ASSERTION_URN)))); + + // Create resolver + EntityClient mockClient = Mockito.mock(EntityClient.class); + GraphClient mockGraphClient = Mockito.mock(GraphClient.class); + initMockGraphClient(mockGraphClient, null); + initMockEntityClient(mockClient, null, props); // No existing contract + UpsertDataContractResolver resolver = + new UpsertDataContractResolver(mockClient, mockGraphClient); + + // Execute resolver + QueryContext mockContext = getMockAllowContext(); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_CREATE_INPUT); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + DataContract result = resolver.get(mockEnv).get(); + + final MetadataChangeProposal propertiesProposal = new MetadataChangeProposal(); + propertiesProposal.setEntityUrn(dataContractUrn); + propertiesProposal.setEntityType(Constants.DATA_CONTRACT_ENTITY_NAME); + propertiesProposal.setSystemMetadata( + new SystemMetadata().setProperties(new StringMap(ImmutableMap.of("appSource", "ui")))); + propertiesProposal.setAspectName(Constants.DATA_CONTRACT_PROPERTIES_ASPECT_NAME); + propertiesProposal.setAspect(GenericRecordUtils.serializeAspect(props)); + propertiesProposal.setChangeType(ChangeType.UPSERT); + + final MetadataChangeProposal statusProposal = new MetadataChangeProposal(); + statusProposal.setEntityUrn(dataContractUrn); + statusProposal.setEntityType(Constants.DATA_CONTRACT_ENTITY_NAME); + statusProposal.setSystemMetadata( + new SystemMetadata().setProperties(new StringMap(ImmutableMap.of("appSource", "ui")))); + statusProposal.setAspectName(Constants.DATA_CONTRACT_STATUS_ASPECT_NAME); + statusProposal.setAspect(GenericRecordUtils.serializeAspect(status)); + statusProposal.setChangeType(ChangeType.UPSERT); + + Mockito.verify(mockClient, Mockito.times(1)) + .batchIngestProposals( + any(OperationContext.class), + Mockito.eq(ImmutableList.of(propertiesProposal, statusProposal)), + Mockito.eq(false)); + + Assert.assertEquals(result.getUrn(), TEST_CONTRACT_URN.toString()); + } + + @Test + public void testGetSuccessUpdate() throws Exception { + + DataContractProperties props = new DataContractProperties(); + props.setEntity(TEST_DATASET_URN); + props.setDataQuality( + new DataQualityContractArray( + ImmutableList.of(new DataQualityContract().setAssertion(TEST_QUALITY_ASSERTION_URN)))); + props.setFreshness( + new FreshnessContractArray( + ImmutableList.of(new FreshnessContract().setAssertion(TEST_FRESHNESS_ASSERTION_URN)))); + props.setSchema( + new SchemaContractArray( + ImmutableList.of(new SchemaContract().setAssertion(TEST_SCHEMA_ASSERTION_URN)))); + + DataContractStatus status = new DataContractStatus(); + status.setState(com.linkedin.datacontract.DataContractState.ACTIVE); + + // Update resolver + EntityClient mockClient = Mockito.mock(EntityClient.class); + GraphClient mockGraphClient = Mockito.mock(GraphClient.class); + initMockGraphClient(mockGraphClient, TEST_CONTRACT_URN); + initMockEntityClient(mockClient, TEST_CONTRACT_URN, props); // Contract Exists + UpsertDataContractResolver resolver = + new UpsertDataContractResolver(mockClient, mockGraphClient); + + // Execute resolver + QueryContext mockContext = getMockAllowContext(); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_VALID_UPDATE_INPUT); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + DataContract result = resolver.get(mockEnv).get(); + + final MetadataChangeProposal propertiesProposal = new MetadataChangeProposal(); + propertiesProposal.setEntityUrn(TEST_CONTRACT_URN); + propertiesProposal.setEntityType(Constants.DATA_CONTRACT_ENTITY_NAME); + propertiesProposal.setSystemMetadata( + new SystemMetadata().setProperties(new StringMap(ImmutableMap.of("appSource", "ui")))); + propertiesProposal.setAspectName(Constants.DATA_CONTRACT_PROPERTIES_ASPECT_NAME); + propertiesProposal.setAspect(GenericRecordUtils.serializeAspect(props)); + propertiesProposal.setChangeType(ChangeType.UPSERT); + + final MetadataChangeProposal statusProposal = new MetadataChangeProposal(); + statusProposal.setEntityUrn(TEST_CONTRACT_URN); + statusProposal.setEntityType(Constants.DATA_CONTRACT_ENTITY_NAME); + statusProposal.setSystemMetadata( + new SystemMetadata().setProperties(new StringMap(ImmutableMap.of("appSource", "ui")))); + statusProposal.setAspectName(Constants.DATA_CONTRACT_STATUS_ASPECT_NAME); + statusProposal.setAspect(GenericRecordUtils.serializeAspect(status)); + statusProposal.setChangeType(ChangeType.UPSERT); + + Mockito.verify(mockClient, Mockito.times(1)) + .batchIngestProposals( + any(OperationContext.class), + Mockito.eq(ImmutableList.of(propertiesProposal, statusProposal)), + Mockito.eq(false)); + + Assert.assertEquals(result.getUrn(), TEST_CONTRACT_URN.toString()); + } + + @Test + public void testGetFailureEntityDoesNotExist() throws Exception { + // Update resolver + EntityClient mockClient = Mockito.mock(EntityClient.class); + GraphClient mockGraphClient = Mockito.mock(GraphClient.class); + initMockGraphClient(mockGraphClient, TEST_CONTRACT_URN); + Mockito.when(mockClient.exists(any(OperationContext.class), Mockito.eq(TEST_DATASET_URN))) + .thenReturn(false); + UpsertDataContractResolver resolver = + new UpsertDataContractResolver(mockClient, mockGraphClient); + + // Execute resolver + QueryContext mockContext = getMockAllowContext(); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_CREATE_INPUT); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + Assert.assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join()); + } + + @Test + public void testGetFailureAssertionDoesNotExist() throws Exception { + // Update resolver + EntityClient mockClient = Mockito.mock(EntityClient.class); + GraphClient mockGraphClient = Mockito.mock(GraphClient.class); + initMockGraphClient(mockGraphClient, TEST_CONTRACT_URN); + Mockito.when(mockClient.exists(any(OperationContext.class), Mockito.eq(TEST_DATASET_URN))) + .thenReturn(true); + Mockito.when( + mockClient.exists( + any(OperationContext.class), Mockito.eq(TEST_FRESHNESS_ASSERTION_URN))) + .thenReturn(false); + Mockito.when( + mockClient.exists(any(OperationContext.class), Mockito.eq(TEST_QUALITY_ASSERTION_URN))) + .thenReturn(false); + Mockito.when( + mockClient.exists(any(OperationContext.class), Mockito.eq(TEST_SCHEMA_ASSERTION_URN))) + .thenReturn(false); + UpsertDataContractResolver resolver = + new UpsertDataContractResolver(mockClient, mockGraphClient); + + // Execute resolver + QueryContext mockContext = getMockAllowContext(); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_CREATE_INPUT); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + Assert.assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join()); + } + + @Test + public void testGetUnauthorized() throws Exception { + // Create resolver + EntityClient mockClient = Mockito.mock(EntityClient.class); + GraphClient mockGraphClient = Mockito.mock(GraphClient.class); + UpsertDataContractResolver resolver = + new UpsertDataContractResolver(mockClient, mockGraphClient); + + // Execute resolver + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + QueryContext mockContext = getMockDenyContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_CREATE_INPUT); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join()); + Mockito.verify(mockClient, Mockito.times(0)) + .ingestProposal(any(OperationContext.class), Mockito.any()); + } + + @Test + public void testGetEntityClientException() throws Exception { + // Create resolver + EntityClient mockClient = Mockito.mock(EntityClient.class); + GraphClient mockGraphClient = Mockito.mock(GraphClient.class); + Mockito.doThrow(RemoteInvocationException.class) + .when(mockClient) + .ingestProposal(any(OperationContext.class), Mockito.any(), Mockito.eq(false)); + UpsertDataContractResolver resolver = + new UpsertDataContractResolver(mockClient, mockGraphClient); + + // Execute resolver + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + QueryContext mockContext = getMockAllowContext(); + Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_CREATE_INPUT); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join()); + } + + private void initMockGraphClient(GraphClient client, Urn existingContractUrn) { + if (existingContractUrn != null) { + Mockito.when( + client.getRelatedEntities( + Mockito.eq(TEST_DATASET_URN.toString()), + Mockito.eq(ImmutableList.of(CONTRACT_FOR_RELATIONSHIP)), + Mockito.eq(RelationshipDirection.INCOMING), + Mockito.eq(0), + Mockito.eq(1), + Mockito.anyString())) + .thenReturn( + new EntityRelationships() + .setTotal(1) + .setCount(1) + .setStart(0) + .setRelationships( + new EntityRelationshipArray( + ImmutableList.of( + new EntityRelationship() + .setEntity(existingContractUrn) + .setType(CONTRACT_FOR_RELATIONSHIP) + .setCreated( + new AuditStamp().setActor(TEST_ACTOR_URN).setTime(0L)))))); + } else { + Mockito.when( + client.getRelatedEntities( + Mockito.eq(TEST_DATASET_URN.toString()), + Mockito.eq(ImmutableList.of(CONTRACT_FOR_RELATIONSHIP)), + Mockito.eq(RelationshipDirection.INCOMING), + Mockito.eq(0), + Mockito.eq(1), + Mockito.anyString())) + .thenReturn( + new EntityRelationships() + .setTotal(0) + .setCount(0) + .setStart(0) + .setRelationships(new EntityRelationshipArray(Collections.emptyList()))); + } + } + + private void initMockEntityClient( + EntityClient client, Urn existingContractUrn, DataContractProperties newContractProperties) + throws Exception { + if (existingContractUrn != null) { + Mockito.when(client.exists(any(OperationContext.class), Mockito.eq(existingContractUrn))) + .thenReturn(true); + } + Mockito.when(client.exists(any(OperationContext.class), Mockito.eq(TEST_DATASET_URN))) + .thenReturn(true); + Mockito.when(client.exists(any(OperationContext.class), Mockito.eq(TEST_QUALITY_ASSERTION_URN))) + .thenReturn(true); + Mockito.when( + client.exists(any(OperationContext.class), Mockito.eq(TEST_FRESHNESS_ASSERTION_URN))) + .thenReturn(true); + Mockito.when(client.exists(any(OperationContext.class), Mockito.eq(TEST_SCHEMA_ASSERTION_URN))) + .thenReturn(true); + + Mockito.when( + client.getV2( + any(OperationContext.class), + Mockito.eq(Constants.DATA_CONTRACT_ENTITY_NAME), + Mockito.eq(TEST_CONTRACT_URN), + Mockito.eq(null))) + .thenReturn( + new EntityResponse() + .setUrn(TEST_CONTRACT_URN) + .setAspects( + new EnvelopedAspectMap( + ImmutableMap.of( + Constants.DATA_CONTRACT_PROPERTIES_ASPECT_NAME, + new EnvelopedAspect() + .setType(AspectType.VERSIONED) + .setName(Constants.DATA_CONTRACT_ENTITY_NAME) + .setValue(new Aspect(newContractProperties.data())))))); + } +} diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/datacontract/DataContractMapperTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/datacontract/DataContractMapperTest.java new file mode 100644 index 00000000000000..49f5a985ea4a3c --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/datacontract/DataContractMapperTest.java @@ -0,0 +1,180 @@ +package com.linkedin.datahub.graphql.types.datacontract; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.linkedin.common.urn.Urn; +import com.linkedin.data.template.StringMap; +import com.linkedin.datacontract.DataContractProperties; +import com.linkedin.datacontract.DataContractState; +import com.linkedin.datacontract.DataContractStatus; +import com.linkedin.datacontract.DataQualityContract; +import com.linkedin.datacontract.DataQualityContractArray; +import com.linkedin.datacontract.FreshnessContract; +import com.linkedin.datacontract.FreshnessContractArray; +import com.linkedin.datacontract.SchemaContract; +import com.linkedin.datacontract.SchemaContractArray; +import com.linkedin.datahub.graphql.generated.DataContract; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.entity.Aspect; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.EnvelopedAspect; +import com.linkedin.entity.EnvelopedAspectMap; +import com.linkedin.metadata.Constants; +import java.util.Collections; +import org.testng.annotations.Test; + +public class DataContractMapperTest { + + @Test + public void testMapAllFields() throws Exception { + EntityResponse entityResponse = new EntityResponse(); + Urn urn = Urn.createFromString("urn:li:dataContract:1"); + Urn dataQualityAssertionUrn = Urn.createFromString("urn:li:assertion:quality"); + Urn dataQualityAssertionUrn2 = Urn.createFromString("urn:li:assertion:quality2"); + + Urn freshnessAssertionUrn = Urn.createFromString("urn:li:assertion:freshness"); + Urn schemaAssertionUrn = Urn.createFromString("urn:li:assertion:schema"); + Urn datasetUrn = + Urn.createFromString("urn:li:dataset:(urn:li:dataPlatform:snowflake,test,PROD)"); + entityResponse.setUrn(urn); + + EnvelopedAspect envelopedDataContractProperties = new EnvelopedAspect(); + DataContractProperties dataContractProperties = new DataContractProperties(); + dataContractProperties.setDataQuality( + new DataQualityContractArray( + ImmutableList.of( + new DataQualityContract().setAssertion(dataQualityAssertionUrn), + new DataQualityContract().setAssertion(dataQualityAssertionUrn2)))); + dataContractProperties.setFreshness( + new FreshnessContractArray( + ImmutableList.of(new FreshnessContract().setAssertion(freshnessAssertionUrn)))); + dataContractProperties.setSchema( + new SchemaContractArray( + ImmutableList.of(new SchemaContract().setAssertion(schemaAssertionUrn)))); + + dataContractProperties.setEntity(datasetUrn); + + envelopedDataContractProperties.setValue(new Aspect(dataContractProperties.data())); + + EnvelopedAspect envelopedDataContractStatus = new EnvelopedAspect(); + DataContractStatus status = new DataContractStatus(); + status.setState(DataContractState.PENDING); + status.setCustomProperties(new StringMap(ImmutableMap.of("key", "value"))); + + envelopedDataContractStatus.setValue(new Aspect(status.data())); + entityResponse.setAspects( + new EnvelopedAspectMap( + Collections.singletonMap( + Constants.DATA_CONTRACT_PROPERTIES_ASPECT_NAME, envelopedDataContractProperties))); + + entityResponse.setAspects( + new EnvelopedAspectMap( + ImmutableMap.of( + Constants.DATA_CONTRACT_PROPERTIES_ASPECT_NAME, + envelopedDataContractProperties, + Constants.DATA_CONTRACT_STATUS_ASPECT_NAME, + envelopedDataContractStatus))); + + DataContract dataContract = DataContractMapper.mapContract(entityResponse); + assertNotNull(dataContract); + assertEquals(dataContract.getUrn(), urn.toString()); + assertEquals(dataContract.getType(), EntityType.DATA_CONTRACT); + assertEquals( + dataContract.getStatus().getState(), + com.linkedin.datahub.graphql.generated.DataContractState.PENDING); + assertEquals(dataContract.getProperties().getEntityUrn(), datasetUrn.toString()); + assertEquals(dataContract.getProperties().getDataQuality().size(), 2); + assertEquals( + dataContract.getProperties().getDataQuality().get(0).getAssertion().getUrn(), + dataQualityAssertionUrn.toString()); + assertEquals( + dataContract.getProperties().getDataQuality().get(1).getAssertion().getUrn(), + dataQualityAssertionUrn2.toString()); + assertEquals(dataContract.getProperties().getFreshness().size(), 1); + assertEquals( + dataContract.getProperties().getFreshness().get(0).getAssertion().getUrn(), + freshnessAssertionUrn.toString()); + assertEquals(dataContract.getProperties().getSchema().size(), 1); + assertEquals( + dataContract.getProperties().getSchema().get(0).getAssertion().getUrn(), + schemaAssertionUrn.toString()); + } + + @Test + public void testMapRequiredFields() throws Exception { + EntityResponse entityResponse = new EntityResponse(); + Urn urn = Urn.createFromString("urn:li:dataContract:1"); + Urn datasetUrn = + Urn.createFromString("urn:li:dataset:(urn:li:dataPlatform:snowflake,test,PROD)"); + entityResponse.setUrn(urn); + + EnvelopedAspect envelopedDataContractProperties = new EnvelopedAspect(); + DataContractProperties dataContractProperties = new DataContractProperties(); + dataContractProperties.setEntity(datasetUrn); + envelopedDataContractProperties.setValue(new Aspect(dataContractProperties.data())); + + EnvelopedAspect envelopedDataContractStatus = new EnvelopedAspect(); + DataContractStatus status = new DataContractStatus(); + status.setState(DataContractState.PENDING); + status.setCustomProperties(new StringMap(ImmutableMap.of("key", "value"))); + + envelopedDataContractStatus.setValue(new Aspect(status.data())); + entityResponse.setAspects( + new EnvelopedAspectMap( + Collections.singletonMap( + Constants.DATA_CONTRACT_PROPERTIES_ASPECT_NAME, envelopedDataContractProperties))); + + entityResponse.setAspects( + new EnvelopedAspectMap( + ImmutableMap.of( + Constants.DATA_CONTRACT_PROPERTIES_ASPECT_NAME, + envelopedDataContractProperties, + Constants.DATA_CONTRACT_STATUS_ASPECT_NAME, + envelopedDataContractStatus))); + + DataContract dataContract = DataContractMapper.mapContract(entityResponse); + assertNotNull(dataContract); + assertEquals(dataContract.getUrn(), urn.toString()); + assertEquals(dataContract.getType(), EntityType.DATA_CONTRACT); + assertEquals( + dataContract.getStatus().getState(), + com.linkedin.datahub.graphql.generated.DataContractState.PENDING); + assertEquals(dataContract.getProperties().getEntityUrn(), datasetUrn.toString()); + assertNull(dataContract.getProperties().getDataQuality()); + assertNull(dataContract.getProperties().getSchema()); + assertNull(dataContract.getProperties().getFreshness()); + } + + @Test + public void testMapNoStatus() throws Exception { + EntityResponse entityResponse = new EntityResponse(); + Urn urn = Urn.createFromString("urn:li:dataContract:1"); + Urn datasetUrn = + Urn.createFromString("urn:li:dataset:(urn:li:dataPlatform:snowflake,test,PROD)"); + entityResponse.setUrn(urn); + + EnvelopedAspect envelopedDataContractProperties = new EnvelopedAspect(); + DataContractProperties dataContractProperties = new DataContractProperties(); + dataContractProperties.setEntity(datasetUrn); + envelopedDataContractProperties.setValue(new Aspect(dataContractProperties.data())); + + entityResponse.setAspects( + new EnvelopedAspectMap( + ImmutableMap.of( + Constants.DATA_CONTRACT_PROPERTIES_ASPECT_NAME, envelopedDataContractProperties))); + + DataContract dataContract = DataContractMapper.mapContract(entityResponse); + assertNotNull(dataContract); + assertEquals(dataContract.getUrn(), urn.toString()); + assertEquals(dataContract.getType(), EntityType.DATA_CONTRACT); + assertNull(dataContract.getStatus()); + assertEquals(dataContract.getProperties().getEntityUrn(), datasetUrn.toString()); + assertNull(dataContract.getProperties().getDataQuality()); + assertNull(dataContract.getProperties().getSchema()); + assertNull(dataContract.getProperties().getFreshness()); + } +} diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/datacontract/DataContractTypeTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/datacontract/DataContractTypeTest.java new file mode 100644 index 00000000000000..241775e5ab48cc --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/datacontract/DataContractTypeTest.java @@ -0,0 +1,152 @@ +package com.linkedin.datahub.graphql.types.datacontract; + +import static com.linkedin.datahub.graphql.TestUtils.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.nullable; +import static org.testng.Assert.*; + +import com.datahub.authentication.Authentication; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datacontract.DataContractProperties; +import com.linkedin.datacontract.DataContractState; +import com.linkedin.datacontract.DataContractStatus; +import com.linkedin.datacontract.DataQualityContract; +import com.linkedin.datacontract.DataQualityContractArray; +import com.linkedin.datacontract.FreshnessContract; +import com.linkedin.datacontract.FreshnessContractArray; +import com.linkedin.datacontract.SchemaContract; +import com.linkedin.datacontract.SchemaContractArray; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.DataContract; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.entity.Aspect; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.EnvelopedAspect; +import com.linkedin.entity.EnvelopedAspectMap; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.key.DataContractKey; +import com.linkedin.r2.RemoteInvocationException; +import graphql.execution.DataFetcherResult; +import io.datahubproject.metadata.context.OperationContext; +import java.util.HashSet; +import java.util.List; +import org.mockito.Mockito; +import org.testng.annotations.Test; + +public class DataContractTypeTest { + + private static final Urn TEST_DATASET_URN = + UrnUtils.getUrn("urn:li:dataset:(urn:li:dataPlatform:snowflake,test,PROD)"); + private static final Urn DATA_QUALITY_ASSERTION_URN = UrnUtils.getUrn("urn:li:assertion:quality"); + private static final Urn FRESHNESS_ASSERTION_URN = UrnUtils.getUrn("urn:li:assertion:freshness"); + private static final Urn SCHEMA_ASSERTION_URN = UrnUtils.getUrn("urn:li:assertion:schema"); + private static final String TEST_DATA_CONTRACT_1_URN = "urn:li:dataContract:id-1"; + private static final DataContractKey TEST_DATA_CONTRACT_1_KEY = + new DataContractKey().setId("id-1"); + private static final DataContractProperties TEST_DATA_CONTRACT_1_PROPERTIES = + new DataContractProperties() + .setEntity(TEST_DATASET_URN) + .setDataQuality( + new DataQualityContractArray( + ImmutableList.of( + new DataQualityContract().setAssertion(DATA_QUALITY_ASSERTION_URN)))) + .setFreshness( + new FreshnessContractArray( + ImmutableList.of(new FreshnessContract().setAssertion(FRESHNESS_ASSERTION_URN)))) + .setSchema( + new SchemaContractArray( + ImmutableList.of(new SchemaContract().setAssertion(SCHEMA_ASSERTION_URN)))); + private static final DataContractStatus TEST_DATA_CONTRACT_1_STATUS = + new DataContractStatus().setState(DataContractState.ACTIVE); + + private static final String TEST_DATA_CONTRACT_2_URN = "urn:li:dataContract:id-2"; + + @Test + public void testBatchLoad() throws Exception { + + EntityClient client = Mockito.mock(EntityClient.class); + + Urn dataContractUrn1 = Urn.createFromString(TEST_DATA_CONTRACT_1_URN); + Urn dataContractUrn2 = Urn.createFromString(TEST_DATA_CONTRACT_2_URN); + + Mockito.when( + client.batchGetV2( + any(OperationContext.class), + Mockito.eq(Constants.DATA_CONTRACT_ENTITY_NAME), + Mockito.eq(new HashSet<>(ImmutableSet.of(dataContractUrn1, dataContractUrn2))), + Mockito.eq(DataContractType.ASPECTS_TO_FETCH))) + .thenReturn( + ImmutableMap.of( + dataContractUrn1, + new EntityResponse() + .setEntityName(Constants.DATA_CONTRACT_ENTITY_NAME) + .setUrn(dataContractUrn1) + .setAspects( + new EnvelopedAspectMap( + ImmutableMap.of( + Constants.DATA_CONTRACT_KEY_ASPECT_NAME, + new EnvelopedAspect() + .setValue(new Aspect(TEST_DATA_CONTRACT_1_KEY.data())), + Constants.DATA_CONTRACT_PROPERTIES_ASPECT_NAME, + new EnvelopedAspect() + .setValue(new Aspect(TEST_DATA_CONTRACT_1_PROPERTIES.data())), + Constants.DATA_CONTRACT_STATUS_ASPECT_NAME, + new EnvelopedAspect() + .setValue(new Aspect(TEST_DATA_CONTRACT_1_STATUS.data()))))))); + + DataContractType type = new DataContractType(client); + + QueryContext mockContext = getMockAllowContext(); + List> result = + type.batchLoad( + ImmutableList.of(TEST_DATA_CONTRACT_1_URN, TEST_DATA_CONTRACT_2_URN), mockContext); + + // Verify response + Mockito.verify(client, Mockito.times(1)) + .batchGetV2( + any(OperationContext.class), + Mockito.eq(Constants.DATA_CONTRACT_ENTITY_NAME), + Mockito.eq(ImmutableSet.of(dataContractUrn1, dataContractUrn2)), + Mockito.eq(DataContractType.ASPECTS_TO_FETCH)); + + assertEquals(result.size(), 2); + + DataContract dataContract1 = result.get(0).getData(); + assertEquals(dataContract1.getUrn(), TEST_DATA_CONTRACT_1_URN); + assertEquals(dataContract1.getType(), EntityType.DATA_CONTRACT); + assertEquals(dataContract1.getProperties().getEntityUrn(), TEST_DATASET_URN.toString()); + assertEquals(dataContract1.getProperties().getDataQuality().size(), 1); + assertEquals(dataContract1.getProperties().getSchema().size(), 1); + assertEquals(dataContract1.getProperties().getFreshness().size(), 1); + + // Assert second element is null. + assertNull(result.get(1)); + } + + @Test + public void testBatchLoadClientException() throws Exception { + EntityClient mockClient = Mockito.mock(EntityClient.class); + Mockito.doThrow(RemoteInvocationException.class) + .when(mockClient) + .batchGetV2( + nullable(OperationContext.class), + Mockito.anyString(), + Mockito.anySet(), + Mockito.anySet()); + DataContractType type = new DataContractType(mockClient); + + // Execute Batch load + QueryContext context = Mockito.mock(QueryContext.class); + Mockito.when(context.getAuthentication()).thenReturn(Mockito.mock(Authentication.class)); + assertThrows( + RuntimeException.class, + () -> + type.batchLoad( + ImmutableList.of(TEST_DATA_CONTRACT_1_URN, TEST_DATA_CONTRACT_2_URN), context)); + } +} diff --git a/li-utils/src/main/java/com/linkedin/metadata/Constants.java b/li-utils/src/main/java/com/linkedin/metadata/Constants.java index 66ed48a428a216..ceab42ae189aad 100644 --- a/li-utils/src/main/java/com/linkedin/metadata/Constants.java +++ b/li-utils/src/main/java/com/linkedin/metadata/Constants.java @@ -362,6 +362,12 @@ public class Constants { public static final String DATAHUB_CONNECTION_ENTITY_NAME = "dataHubConnection"; public static final String DATAHUB_CONNECTION_DETAILS_ASPECT_NAME = "dataHubConnectionDetails"; + // Data Contracts + public static final String DATA_CONTRACT_ENTITY_NAME = "dataContract"; + public static final String DATA_CONTRACT_PROPERTIES_ASPECT_NAME = "dataContractProperties"; + public static final String DATA_CONTRACT_KEY_ASPECT_NAME = "dataContractKey"; + public static final String DATA_CONTRACT_STATUS_ASPECT_NAME = "dataContractStatus"; + // Relationships public static final String IS_MEMBER_OF_GROUP_RELATIONSHIP_NAME = "IsMemberOfGroup"; public static final String IS_MEMBER_OF_NATIVE_GROUP_RELATIONSHIP_NAME = "IsMemberOfNativeGroup"; diff --git a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java index ff740a4dfc0e05..10af24f5876ef1 100644 --- a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java +++ b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java @@ -345,6 +345,12 @@ public class PoliciesConfig { "Edit Dataset Queries", "The ability to edit the Queries for a Dataset."); + public static final Privilege EDIT_ENTITY_DATA_CONTRACT_PRIVILEGE = + Privilege.of( + "EDIT_ENTITY_DATA_CONTRACT", + "Edit Data Contract", + "The ability to edit the Data Contract for an entity."); + // Tag Privileges public static final Privilege EDIT_TAG_COLOR_PRIVILEGE = Privilege.of("EDIT_TAG_COLOR", "Edit Tag Color", "The ability to change the color of a Tag."); From a240d125adbf4a46f1a63ebb455e5ed0d8e1b166 Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Fri, 31 May 2024 00:07:20 +0530 Subject: [PATCH 02/26] feat(data-contract): Add initial file for data contract on validation tab --- .../Dataset/Validations/ValidationsTab.tsx | 26 +++- .../Validations/contract/DataContractTab.tsx | 118 ++++++++++++++++++ datahub-web-react/src/appConfigContext.tsx | 1 + 3 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/ValidationsTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/ValidationsTab.tsx index b4f77196edbb1b..c4e9084d67c7a4 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/ValidationsTab.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/ValidationsTab.tsx @@ -2,13 +2,15 @@ import React, { useEffect } from 'react'; import { Button } from 'antd'; import { useHistory, useLocation } from 'react-router'; import styled from 'styled-components'; -import { FileDoneOutlined, FileProtectOutlined } from '@ant-design/icons'; +import { AuditOutlined, FileDoneOutlined, FileProtectOutlined } from '@ant-design/icons'; import { useEntityData } from '../../../EntityContext'; import { TestResults } from './TestResults'; import { Assertions } from './Assertions'; import TabToolbar from '../../../components/styled/TabToolbar'; import { useGetValidationsTab } from './useGetValidationsTab'; import { ANTD_GRAY } from '../../../constants'; +import { useAppConfig } from '../../../../../useAppConfig'; +import { DataContractTab } from './contract/DataContractTab'; const TabTitle = styled.span` margin-left: 4px; @@ -22,6 +24,7 @@ const TabButton = styled(Button)<{ selected: boolean }>` enum TabPaths { ASSERTIONS = 'Assertions', TESTS = 'Tests', + DATA_CONTRACT = 'Data Contract', } const DEFAULT_TAB = TabPaths.ASSERTIONS; @@ -33,6 +36,7 @@ export const ValidationsTab = () => { const { entityData } = useEntityData(); const history = useHistory(); const { pathname } = useLocation(); + const appConfig = useAppConfig(); const totalAssertions = (entityData as any)?.assertions?.total; const passingTests = (entityData as any)?.testResults?.passing || []; @@ -77,6 +81,26 @@ export const ValidationsTab = () => { }, ]; + // if (appConfig.config.featureFlags?.dataContractsEnabled) { + // If contracts feature is enabled, add to list. + + tabs.push({ + title: ( + <> + + + Data Contract + + ), + + path: TabPaths.DATA_CONTRACT, + + content: , + + disabled: false, + }); + // } + return ( <> diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx new file mode 100644 index 00000000000000..5c009406b30d22 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx @@ -0,0 +1,118 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +// import { useGetDatasetContractQuery } from '../../../../../../../graphql/contract.generated'; +// import { DataContractState } from '../../../../../../../types.generated'; +import { useEntityData } from '../../../../EntityContext'; +// import { DataContractSummary } from './DataContractSummary'; +// import { DataQualityContractSummary } from './DataQualityContractSummary'; +// import { SchemaContractSummary } from './SchemaContractSummary'; +// import { FreshnessContractSummary } from './FreshnessContractSummary'; +// import { DataContractBuilderModal } from './builder/DataContractBuilderModal'; +// import { createBuilderState } from './builder/utils'; +// import { getAssertionsSummary } from '../DatahubUtility'; + +const Container = styled.div` + display: flex; +`; + +const LeftColumn = styled.div` + width: 50%; +`; + +const RightColumn = styled.div` + width: 50%; +`; + +/** + * Component used for rendering the Data Contract Tab on the Assertions parent tab. + */ +export const DataContractTab = () => { + // const { urn, entityType } = useEntityData(); + + // const { data, refetch } = useGetDatasetContractQuery({ + // variables: { + // urn, + // }, + // }); + // const [showContractBuilder, setShowContractBuilder] = useState(false); + + // const contract = data?.dataset?.contract; + // const schemaContracts = data?.dataset?.contract?.properties?.schema || []; + // const freshnessContracts = data?.dataset?.contract?.properties?.freshness || []; + // const dataQualityContracts = data?.dataset?.contract?.properties?.dataQuality || []; + // const schemaAssertions = schemaContracts.map((c) => c.assertion); + // const freshnessAssertions = freshnessContracts.map((c) => c.assertion); + // const dataQualityAssertions = dataQualityContracts.map((c) => c.assertion); + // const assertionsSummary = getAssertionsSummary([ + // ...schemaAssertions, + // ...freshnessAssertions, + // ...dataQualityAssertions, + // ] as any); + // const contractState = data?.dataset?.contract?.status?.state || DataContractState.Active; + // const hasFreshnessContract = freshnessContracts && freshnessContracts?.length; + // const hasSchemaContract = schemaContracts && schemaContracts?.length; + // const hasDataQualityContract = dataQualityContracts && dataQualityContracts?.length; + // const showLeftColumn = hasFreshnessContract || hasSchemaContract || undefined; + + // const onContractUpdate = () => { + // if (contract) { + // // Contract exists, just refetch. + // refetch(); + // } else { + // // no contract yet, wait for indxing, + // setTimeout(() => refetch(), 3000); + // } + // setShowContractBuilder(false); + // }; + + // return ( + // <> + // {data?.dataset?.contract && ( + // <> + // setShowContractBuilder(true)} + // /> + // + // {showLeftColumn && ( + // + // {(hasFreshnessContract && ( + // + // )) || + // undefined} + // {(hasSchemaContract && ( + // + // )) || + // undefined} + // + // )} + // + // {(hasDataQualityContract && ( + // + // )) || + // undefined} + // + // + // + // )} + // {showContractBuilder && ( + // setShowContractBuilder(false)} + // onPropose={onContractUpdate} + // onSubmit={onContractUpdate} + // entityType={entityType} + // /> + // )} + // + // ); + return
DataContractTab
; +}; diff --git a/datahub-web-react/src/appConfigContext.tsx b/datahub-web-react/src/appConfigContext.tsx index b4f16e2d2a8240..8f7bb0e0b5e47c 100644 --- a/datahub-web-react/src/appConfigContext.tsx +++ b/datahub-web-react/src/appConfigContext.tsx @@ -53,6 +53,7 @@ export const DEFAULT_APP_CONFIG = { nestedDomainsEnabled: true, platformBrowseV2: false, businessAttributeEntityEnabled: false, + dataContractsEnabled: true, }, }; From 2704ca7254956b9c0eee853dc6796a7afec6b023 Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Fri, 31 May 2024 13:50:24 +0530 Subject: [PATCH 03/26] feat(data-contract): add datacontract tab file and its graphql files --- datahub-web-react/package.json | 2 + datahub-web-react/src/app/analytics/event.ts | 1 + .../Validations/FieldAssertionDescription.tsx | 31 ++ .../Validations/SqlAssertionDescription.tsx | 16 + .../VolumeAssertionDescription.tsx | 87 ++++ .../tabs/Dataset/Validations/acrylTypes.tsx | 21 + .../tabs/Dataset/Validations/acrylUtils.tsx | 447 ++++++++++++++++++ .../contract/DataContractAssertionStatus.tsx | 44 ++ .../contract/DataContractSummary.tsx | 95 ++++ .../contract/DataContractSummaryFooter.tsx | 75 +++ .../Validations/contract/DataContractTab.tsx | 194 ++++---- .../contract/DataQualityContractSummary.tsx | 100 ++++ .../contract/FreshnessContractSummary.tsx | 96 ++++ .../contract/FreshnessScheduleSummary.tsx | 28 ++ .../contract/SchemaContractSummary.tsx | 82 ++++ .../DataContractAssertionGroupSelect.tsx | 57 +++ .../contract/builder/DataContractBuilder.tsx | 250 ++++++++++ .../builder/DataContractBuilderModal.tsx | 78 +++ .../Validations/contract/builder/types.ts | 37 ++ .../Validations/contract/builder/utils.ts | 139 ++++++ .../proposal/DataContractProposal.tsx | 274 +++++++++++ .../DataContractProposalDescription.tsx | 47 ++ .../proposal/DataContractProposalModal.tsx | 54 +++ .../Dataset/Validations/contract/utils.tsx | 109 +++++ .../Validations/fieldDescriptionUtils.ts | 149 ++++++ .../Validations/shared/styledComponents.tsx | 32 ++ .../src/app/shared/numberUtil.ts | 24 + .../src/graphql/actionRequest.graphql | 0 .../src/graphql/contract.graphql | 54 +++ datahub-web-react/src/graphql/monitor.graphql | 11 + datahub-web-react/yarn.lock | 44 +- 31 files changed, 2576 insertions(+), 102 deletions(-) create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/FieldAssertionDescription.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/SqlAssertionDescription.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/VolumeAssertionDescription.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylTypes.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylUtils.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractAssertionStatus.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummary.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummaryFooter.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataQualityContractSummary.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/FreshnessContractSummary.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/FreshnessScheduleSummary.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/SchemaContractSummary.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractAssertionGroupSelect.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilderModal.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/types.ts create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/utils.ts create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposal.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposalDescription.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposalModal.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/utils.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/fieldDescriptionUtils.ts create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/shared/styledComponents.tsx create mode 100644 datahub-web-react/src/app/shared/numberUtil.ts create mode 100644 datahub-web-react/src/graphql/actionRequest.graphql create mode 100644 datahub-web-react/src/graphql/contract.graphql create mode 100644 datahub-web-react/src/graphql/monitor.graphql diff --git a/datahub-web-react/package.json b/datahub-web-react/package.json index 50a74bb0f42593..69807801e7ae8f 100644 --- a/datahub-web-react/package.json +++ b/datahub-web-react/package.json @@ -11,6 +11,7 @@ "@apollo/client": "^3.3.19", "@graphql-codegen/fragment-matcher": "^5.0.0", "@monaco-editor/react": "^4.3.1", + "@mui/icons-material": "^5.15.2", "@react-hook/window-size": "^3.0.7", "@react-spring/web": "^9.7.3", "@remirror/pm": "^2.0.3", @@ -40,6 +41,7 @@ "analytics": "^0.8.9", "antd": "4.24.7", "color-hash": "^2.0.1", + "cron-parser": "^4.8.1", "cronstrue": "^1.122.0", "d3-scale": "^4.0.2", "dayjs": "^1.11.7", diff --git a/datahub-web-react/src/app/analytics/event.ts b/datahub-web-react/src/app/analytics/event.ts index d63b731c720426..2d3a3fa781a22f 100644 --- a/datahub-web-react/src/app/analytics/event.ts +++ b/datahub-web-react/src/app/analytics/event.ts @@ -312,6 +312,7 @@ export interface EntityActionEvent extends BaseEvent { actionType: string; entityType?: EntityType; entityUrn: string; + actionQualifier?: string; } export interface BatchEntityActionEvent extends BaseEvent { diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/FieldAssertionDescription.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/FieldAssertionDescription.tsx new file mode 100644 index 00000000000000..f57a1be6f66e13 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/FieldAssertionDescription.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Typography } from 'antd'; +// import { FieldAssertionInfo } from '../../../../../../types.generated'; +import { + getFieldDescription, + getFieldOperatorDescription, + getFieldParametersDescription, + getFieldTransformDescription, +} from './fieldDescriptionUtils'; + +type Props = { + assertionInfo: any; +}; + +/** + * A human-readable description of a Field Assertion. + */ +export const FieldAssertionDescription = ({ assertionInfo }: Props) => { + const field = getFieldDescription(assertionInfo); + const operator = getFieldOperatorDescription(assertionInfo); + const transform = getFieldTransformDescription(assertionInfo); + const parameters = getFieldParametersDescription(assertionInfo); + + return ( + + {transform} + {transform ? ' of ' : ''} + {field} {operator} {parameters} + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/SqlAssertionDescription.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/SqlAssertionDescription.tsx new file mode 100644 index 00000000000000..047f7c7db28f65 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/SqlAssertionDescription.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Typography } from 'antd'; +import { AssertionInfo } from '../../../../../../types.generated'; + +type Props = { + assertionInfo: AssertionInfo; +}; + +/** + * A human-readable description of a SQL Assertion. + */ +export const SqlAssertionDescription = ({ assertionInfo }: Props) => { + const { description } = assertionInfo; + + return {description}; +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/VolumeAssertionDescription.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/VolumeAssertionDescription.tsx new file mode 100644 index 00000000000000..2f8fe9c1a70aa1 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/VolumeAssertionDescription.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { Typography } from 'antd'; +import { + AssertionStdOperator, + AssertionStdParameters, + AssertionValueChangeType, + IncrementingSegmentRowCountChange, + RowCountChange, + VolumeAssertionInfo, + VolumeAssertionType, +} from '../../../../../../types.generated'; +import { getIsRowCountChange, getVolumeTypeInfo } from './assertion/builder/steps/volume/utils'; +import { formatNumberWithoutAbbreviation } from '../../../../../shared/formatNumber'; +import { parseMaybeStringAsFloatOrDefault } from '../../../../../shared/numberUtil'; + +type Props = { + assertionInfo: VolumeAssertionInfo; +}; + +const getVolumeTypeDescription = (volumeType: VolumeAssertionType) => { + switch (volumeType) { + case VolumeAssertionType.RowCountTotal: + case VolumeAssertionType.IncrementingSegmentRowCountTotal: + return 'has'; + case VolumeAssertionType.RowCountChange: + case VolumeAssertionType.IncrementingSegmentRowCountChange: + return 'should grow by'; + default: + throw new Error(`Unknown volume type ${volumeType}`); + } +}; + +const getOperatorDescription = (operator: AssertionStdOperator) => { + switch (operator) { + case AssertionStdOperator.GreaterThanOrEqualTo: + return 'at least'; + case AssertionStdOperator.LessThanOrEqualTo: + return 'at most'; + case AssertionStdOperator.Between: + return 'between'; + default: + throw new Error(`Unknown operator ${operator}`); + } +}; + +const getValueChangeTypeDescription = (valueChangeType: AssertionValueChangeType) => { + switch (valueChangeType) { + case AssertionValueChangeType.Absolute: + return 'rows'; + case AssertionValueChangeType.Percentage: + return '%'; + default: + throw new Error(`Unknown value change type ${valueChangeType}`); + } +}; + +const getParameterDescription = (parameters: AssertionStdParameters) => { + if (parameters.value) { + return formatNumberWithoutAbbreviation(parseMaybeStringAsFloatOrDefault(parameters.value.value, parameters.value.value)); + } + if (parameters.minValue && parameters.maxValue) { + return `${formatNumberWithoutAbbreviation(parseMaybeStringAsFloatOrDefault(parameters.minValue.value, parameters.minValue.value))} and ${formatNumberWithoutAbbreviation(parseMaybeStringAsFloatOrDefault(parameters.maxValue.value, parameters.maxValue.value))}`; + } + throw new Error('Invalid assertion parameters provided'); +}; + +/** + * A human-readable description of a Volume Assertion. + */ +export const VolumeAssertionDescription = ({ assertionInfo }: Props) => { + const volumeType = assertionInfo.type; + const volumeTypeInfo = getVolumeTypeInfo(assertionInfo); + const volumeTypeDescription = getVolumeTypeDescription(volumeType); + const operatorDescription = volumeTypeInfo ? getOperatorDescription(volumeTypeInfo.operator) : ''; + const parameterDescription = volumeTypeInfo ? getParameterDescription(volumeTypeInfo.parameters) : ''; + const valueChangeTypeDescription = getIsRowCountChange(volumeType) + ? getValueChangeTypeDescription((volumeTypeInfo as RowCountChange | IncrementingSegmentRowCountChange).type) + : 'rows'; + + return ( +
+ + Table {volumeTypeDescription} {operatorDescription} {parameterDescription} {valueChangeTypeDescription} + +
+ ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylTypes.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylTypes.tsx new file mode 100644 index 00000000000000..8a70a3d87c1478 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylTypes.tsx @@ -0,0 +1,21 @@ +import { Assertion, AssertionType } from '../../../../../../types.generated'; + +export type AssertionStatusSummary = { + passing: number; + failing: number; + erroring: number; + total: number; // Total assertions with at least 1 run. + totalAssertions: number; +}; + +/** + * A group of assertions related by their logical type or category. + */ +export type AssertionGroup = { + name: string; + icon: React.ReactNode; + description?: string; + assertions: Assertion[]; + summary: AssertionStatusSummary; + type: AssertionType; +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylUtils.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylUtils.tsx new file mode 100644 index 00000000000000..d730f290cb3237 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylUtils.tsx @@ -0,0 +1,447 @@ +import React from 'react'; +import styled from 'styled-components'; +import * as moment from 'moment-timezone'; +import * as cronParser from 'cron-parser'; +import cronstrue from 'cronstrue'; +import { + ClockCircleOutlined, + TableOutlined, + ProjectOutlined, + ConsoleSqlOutlined, + CheckOutlined, + CloseOutlined, + ApiOutlined, + CodeOutlined, + ExclamationCircleOutlined, +} from '@ant-design/icons'; +import { + Assertion, + AssertionResultType, + AssertionType, + // CronSchedule, + // DatasetFreshnessSourceType, + // DatasetVolumeSourceType, + EntityType, + // Monitor, + // MonitorMode, +} from '../../../../../../types.generated'; +import { sortAssertions } from './assertionUtils'; +import { AssertionGroup, AssertionStatusSummary } from './acrylTypes'; +import { lowerFirstLetter } from '../../../../../shared/textUtil'; +// import { useIngestionSourceForEntityQuery } from '../../../../../../graphql/ingestion.generated'; +// import { +// GetDatasetAssertionsWithMonitorsQuery, +// MonitorDetailsFragment, +// } from '../../../../../../graphql/monitor.generated'; + +export const SUCCESS_COLOR_HEX = '#52C41A'; +export const FAILURE_COLOR_HEX = '#F5222D'; +export const WARNING_COLOR_HEX = '#FA8C16'; + +const StyledClockCircleOutlined = styled(ClockCircleOutlined)` + && { + margin: 0px; + padding: 0px; + margin-right: 8px; + font-size: 14px; + } +`; + +const StyledTableOutlined = styled(TableOutlined)` + && { + margin: 0px; + padding: 0px; + margin-right: 8px; + font-size: 18px; + } +`; + +const StyledProjectOutlined = styled(ProjectOutlined)` + && { + margin: 0px; + padding: 0px; + margin-right: 8px; + font-size: 18px; + } +`; + +const StyledConsoleSqlOutlined = styled(ConsoleSqlOutlined)` + && { + margin: 0px; + padding: 0px; + margin-right: 8px; + font-size: 18px; + } +`; + +const StyledApiOutlined = styled(ApiOutlined)` + && { + margin: 0px; + padding: 0px; + margin-right: 8px; + font-size: 18px; + } +`; + +const StyledCheckOutlined = styled(CheckOutlined)` + && { + color: ${SUCCESS_COLOR_HEX}; + font-size: 14px; + padding: 0px; + margin: 0px; + } +`; + +const StyledCloseOutlined = styled(CloseOutlined)` + && { + color: ${FAILURE_COLOR_HEX}; + font-size: 14px; + padding: 0px; + margin: 0px; + } +`; + +const StyledExclamationOutlined = styled(ExclamationCircleOutlined)` + && { + color: ${WARNING_COLOR_HEX}; + font-size: 14px; + padding: 0px; + margin: 0px; + } +`; + +const StyledCodeOutlined = styled(CodeOutlined)` + && { + margin: 0px; + padding: 0px; + margin-right: 8px; + font-size: 18px; + } +`; + +export const ASSERTION_INFO = [ + { + name: 'Freshness', + description: 'Define & monitor your expectations about when this dataset should be updated', + icon: , + type: AssertionType.Freshness, + entityTypes: [EntityType.Dataset], + enabled: true, + visible: true, + }, + { + name: 'Volume', + description: 'Define & monitor your expectations about the size of this dataset', + icon: , + type: AssertionType.Volume, + entityTypes: [EntityType.Dataset], + enabled: true, + visible: true, + }, + { + name: 'Column', + description: 'Define & monitor your expectations about the values in a column', + icon: , + type: AssertionType.Field, + entityTypes: [EntityType.Dataset], + enabled: true, + visible: true, + requiresConnectionSupportedByMonitors: false, + }, + { + name: 'Schema', + description: "Define & monitor your expectations about the table's columns and their types", + icon: , + type: AssertionType.DataSchema, + entityTypes: [EntityType.Dataset], + enabled: true, + visible: true, + }, + { + name: 'Custom', + description: 'Define & monitor your expectations using custom SQL rules', + icon: , + type: AssertionType.Sql, + entityTypes: [EntityType.Dataset], + enabled: true, + visible: true, + requiresConnectionSupportedByMonitors: true, + }, + { + name: 'Other', + description: 'Assertions that are defined and maintained outside of DataHub.', + icon: , + type: AssertionType.Dataset, + entityTypes: [EntityType.Dataset], + enabled: false, + visible: false, + }, +]; + +const ASSERTION_TYPE_TO_INFO = new Map(); +ASSERTION_INFO.forEach((info) => { + ASSERTION_TYPE_TO_INFO.set(info.type, info); +}); + +const getAssertionGroupName = (type: AssertionType): string => { + return ASSERTION_TYPE_TO_INFO.has(type) ? ASSERTION_TYPE_TO_INFO.get(type).name : 'Unknown'; +}; + +const getAssertionGroupTypeIcon = (type: AssertionType) => { + return ASSERTION_TYPE_TO_INFO.has(type) ? ASSERTION_TYPE_TO_INFO.get(type).icon : undefined; +}; + +export type AssertionWithMonitorDetails = Assertion & { + monitors?: any[]; // should almost always have 0-1 items +}; + +// export const tryExtractMonitorDetailsFromAssertionsWithMonitorsQuery = ( +// queryData?: GetDatasetAssertionsWithMonitorsQuery, +// ): AssertionWithMonitorDetails[] | undefined => { +// return queryData?.dataset?.assertions?.assertions?.map((assertion) => ({ +// ...(assertion as Assertion), +// monitors: +// assertion.monitor?.relationships +// ?.filter((r) => r.entity?.__typename === 'Monitor') +// .map((r) => r.entity as MonitorDetailsFragment) ?? [], +// })); +// }; + +/** + * Returns a status summary for the assertions associated with a Dataset. + * + * @param assertions The assertions to extract the summary for + */ +export const getAssertionsSummary = (assertions: AssertionWithMonitorDetails[]): AssertionStatusSummary => { + const summary = { + passing: 0, + failing: 0, + erroring: 0, + total: 0, + totalAssertions: assertions.length, + }; + assertions.forEach((assertion) => { + // Skip inactive monitors + // NOTE: we don't assert that the status is Active, because in cases of external assertions they won't have monitors + const maybeInactiveMonitor = assertion.monitors?.find( + (item) => item.info?.status.mode === 'IN_ACTIVE', //MonitorMode.Inactive, + ); + if (maybeInactiveMonitor) { + return; + } + + if ((assertion.runEvents?.runEvents?.length || 0) > 0) { + const mostRecentRun = assertion.runEvents?.runEvents?.[0]; + const resultType = mostRecentRun?.result?.type; + if (AssertionResultType.Success === resultType) { + summary.passing++; + } + if (AssertionResultType.Failure === resultType) { + summary.failing++; + } + // if (AssertionResultType.Error === resultType) { + // if ('ERROR' === resultType) { + // summary.erroring++; + // } + // if ('INIT' !== resultType) { + // summary.total++; // only count assertions for which there is one completed run event, ignoring INIT statuses! + // } + } + }); + return summary; +}; + +/** + * TODO: We will remove this mapping code once we replace the OSS legacy assertions summary with the new + * format. + */ +export const getLegacyAssertionsSummary = (assertions: Assertion[]) => { + const newSummary = getAssertionsSummary(assertions); + return { + failedRuns: newSummary.failing, + succeededRuns: newSummary.passing, + erroredRuns: newSummary.erroring, + totalRuns: newSummary.total, + totalAssertions: newSummary.totalAssertions, + }; +}; + +// /** +// * Returns a list of assertion groups, where assertions are grouped +// * by their "type" or "category". Each group includes the assertions inside, along with +// * a summary of passing and failing assertions for the group. +// * +// * @param assertions The assertions to group +// */ +// export const createAssertionGroups = (assertions: Array): AssertionGroup[] => { +// // Pre-sort the list of assertions based on which has been most recently executed. +// assertions.sort(sortAssertions); + +// const typeToAssertions = new Map(); +// assertions +// .filter((assertion) => assertion.info?.type) +// .forEach((assertion) => { +// const groupType = assertion.info?.type; +// const groupedAssertions = typeToAssertions.get(groupType) || []; +// groupedAssertions.push(assertion); +// typeToAssertions.set(groupType, groupedAssertions); +// }); + +// // Now, create summary for each type and build the AssertionGroup object +// const assertionGroups: AssertionGroup[] = []; +// typeToAssertions.forEach((groupedAssertions, type) => { +// const newGroup: AssertionGroup = { +// name: getAssertionGroupName(type), +// icon: getAssertionGroupTypeIcon(type), +// assertions: groupedAssertions, +// summary: getAssertionsSummary(groupedAssertions), +// type, +// }; +// assertionGroups.push(newGroup); +// }); + +// return assertionGroups; +// }; + +// TODO: Make this the default inside DatasetAssertionsSummary.tsx. +export const getAssertionGroupSummaryIcon = (summary: AssertionStatusSummary) => { + if (summary.total === 0) { + return null; + } + if (summary.passing === summary.total) { + return ; + } + if (summary.erroring > 0) { + return ; + } + return ; +}; + +// TODO: Make this the default inside DatasetAssertionsSummary.tsx. +export const getAssertionGroupSummaryMessage = (summary: AssertionStatusSummary) => { + if (summary.total === 0) { + return 'No assertions have run'; + } + if (summary.passing === summary.total) { + return 'All assertions are passing'; + } + if (summary.erroring > 0) { + return 'An error is preventing some assertions from running'; + } + if (summary.failing === summary.total) { + return 'All assertions are failing'; + } + return 'Some assertions are failing'; +}; + +/** + * Returns the next scheduled run of a cron schedule, in the local timezone of teh user. + * + * @param schedule a cron schedule + */ +// export const getNextScheduleEvaluationTimeMs = (schedule: CronSchedule) => { +// try { +// const interval = cronParser.parseExpression(schedule.cron, { tz: schedule.timezone }); +// const nextDate = interval.next().toDate(); // Get next date as JavaScript Date object +// const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; +// const nextDateInUserTz = moment.tz(nextDate, userTimezone); // Convert to user's timezone +// return nextDateInUserTz.valueOf(); +// } catch (e) { +// return undefined; +// } +// }; + +export const getAssertionTypesForEntityType = (entityType: EntityType, monitorsConnectionForEntityExists: boolean) => { + return ASSERTION_INFO.filter((type) => type.entityTypes.includes(entityType)).map((type) => ({ + ...type, + enabled: type.enabled && (!type.requiresConnectionSupportedByMonitors || monitorsConnectionForEntityExists), + })); +}; + +export const isMonitorActive = (monitor: any) => { + return monitor.info?.status?.mode === 'active'; +}; + +export const getCronAsText = (interval: string, options: { verbose: boolean } = { verbose: false }) => { + const { verbose } = options; + if (interval) { + try { + return { + text: `${lowerFirstLetter(cronstrue.toString(interval, { verbose }))}.`, + error: false, + }; + } catch (e) { + return { + text: undefined, + error: true, + }; + } + } + return { + text: undefined, + error: false, + }; +}; + +export const canManageAssertionMonitor = (monitor: any, connectionForEntityExists: boolean) => { + if (connectionForEntityExists) return true; + + const assertionParameters = monitor?.info?.assertionMonitor?.assertions?.[0]?.parameters; + return ( + assertionParameters?.datasetFreshnessParameters?.sourceType === 'DATAHUB_OPERATION' || //DatasetFreshnessSourceType.DatahubOperation || + assertionParameters?.datasetVolumeParameters?.sourceType === 'DATAHUB_DATASET_PROFILE' //DatasetVolumeSourceType.DatahubDatasetProfile + ); +}; + +// export const getEntityUrnForAssertion = (assertion: Assertion) => { +export const getEntityUrnForAssertion = (assertion: any) => { + if (assertion.info?.type === AssertionType.Dataset) { + return assertion.info?.datasetAssertion?.datasetUrn; + } + if (assertion.info?.type === AssertionType.Freshness) { + return assertion.info?.freshnessAssertion?.entityUrn; + } + if (assertion.info?.type === AssertionType.Volume) { + return assertion.info?.volumeAssertion?.entityUrn; + } + if (assertion.info?.type === AssertionType.Field) { + return assertion.info?.fieldAssertion?.entityUrn; + } + if (assertion.info?.type === AssertionType.Sql) { + return assertion.info?.sqlAssertion?.entityUrn; + } + if (assertion.info?.type === AssertionType.DataSchema) { + return assertion.info?.schemaAssertion?.entityUrn; + } + console.error(`Unable to extract entity urn from unrecognized assertion with type ${assertion.info?.type}`); + return undefined; +}; + +// export const useConnectionForEntityExists = (entityUrn: string) => { +// const { data: ingestionSourceData } = useIngestionSourceForEntityQuery({ +// variables: { urn: entityUrn as string }, +// fetchPolicy: 'cache-first', +// }); + +// return !!ingestionSourceData?.ingestionSourceForEntity?.urn; +// }; + +/** + * Checks if a connection exists for an entity that is able to run test assertion queries + * @param entityUrn + * @returns {boolean} optimistically returns true + */ +// export const useConnectionWithTestAssertionCapabilitiesForEntityExists = (entityUrn: string): boolean => { +// const { data: ingestionSourceData } = useIngestionSourceForEntityQuery({ +// variables: { urn: entityUrn as string }, +// fetchPolicy: 'cache-first', +// }); + +// // Only embedded executors can run tests right now +// // If executorId is null, we'll assume it is an embedded executor. +// // If the executorId starts with 'default', we assume it's an embedded executor +// // See setup docs: https://www.notion.so/acryldata/How-to-configure-Remote-Executor-e9ed044b438d4789afcd530952d73944?pvs=4#14237a6d6dd04fcfb2abd45f16c6d63c +// // and design docs: https://www.notion.so/acryldata/Remote-Executor-V2-Design-593d41280c4a4e34805def00b3f47a65?pvs=4#fe2a4481fbe74f379eb35cd10546b3b8 +// const maybeExecutorId = ingestionSourceData?.ingestionSourceForEntity?.config?.executorId; +// return !maybeExecutorId || maybeExecutorId.toLowerCase().startsWith('default'); +// }; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractAssertionStatus.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractAssertionStatus.tsx new file mode 100644 index 00000000000000..c36bef09cdb688 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractAssertionStatus.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Tooltip } from 'antd'; +import { StopOutlined } from '@ant-design/icons'; +import { Assertion, AssertionResultType } from '../../../../../../../types.generated'; +import { + StyledCheckOutlined, + StyledClockCircleOutlined, + StyledCloseOutlined, + StyledExclamationOutlined, +} from '../shared/styledComponents'; + +const StatusContainer = styled.div` + width: 100%; + display: flex; + justify-content: center; +`; + +type Props = { + assertion: Assertion; +}; + +export const DataContractAssertionStatus = ({ assertion }: Props) => { + const latestRun = (assertion.runEvents?.runEvents?.length && assertion.runEvents?.runEvents[0]) || undefined; + const latestResultType = latestRun?.result?.type || undefined; + + return ( + + {latestResultType === undefined && } + + {latestResultType === AssertionResultType.Success && } + + + {latestResultType === AssertionResultType.Failure && } + + + {latestResultType === AssertionResultType.Error && } + + + {latestResultType === AssertionResultType.Init && } + + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummary.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummary.tsx new file mode 100644 index 00000000000000..c51c2b9e9dca08 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummary.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import styled from 'styled-components'; +import EditIcon from '@mui/icons-material/Edit'; +import { Button, Typography } from 'antd'; +import { DataContractState } from '../../../../../../../types.generated'; +import { AssertionStatusSummary } from '../acrylTypes'; +import { getContractSummaryIcon, getContractSummaryTitle, getContractSummaryMessage } from './utils'; +import { ANTD_GRAY } from '../../../../constants'; + +const SummaryHeader = styled.div` + width: 100%; + padding-left: 40px; + padding-top: 20px; + padding-bottom: 20px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid ${ANTD_GRAY[4.5]}; +`; + +const SummaryContainer = styled.div``; + +const SummaryDescription = styled.div` + display: flex; + align-items: center; +`; + +const SummaryMessage = styled.div` + display: inline-block; + margin-left: 20px; +`; + +const SummaryTitle = styled(Typography.Title)` + && { + padding-bottom: 0px; + margin-bottom: 0px; + } +`; + +const Actions = styled.div` + margin: 12px; + margin-right: 20px; +`; + +const CreateButton = styled(Button)` + display: flex; + align-items: center; + gap: 0.3rem; + margin-right: 12px; + border-color: ${(props) => props.theme.styles['primary-color']}; + color: ${(props) => props.theme.styles['primary-color']}; + letter-spacing: 2px; + &&:hover { + color: white; + background-color: ${(props) => props.theme.styles['primary-color']}; + border-color: ${(props) => props.theme.styles['primary-color']}; + } +`; + +const EditIconStyle = styled(EditIcon)` + && { + font-size: 16px; + } +`; + +type Props = { + state: DataContractState; + summary: AssertionStatusSummary; + showContractBuilder: () => void; +}; + +export const DataContractSummary = ({ state, summary, showContractBuilder }: Props) => { + const summaryIcon = getContractSummaryIcon(state, summary); + const summaryTitle = getContractSummaryTitle(state, summary); + const summaryMessage = getContractSummaryMessage(state, summary); + return ( + + + + {summaryIcon} + + {summaryTitle} + {summaryMessage} + + + + + + + EDIT + + + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummaryFooter.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummaryFooter.tsx new file mode 100644 index 00000000000000..29e41553149f7b --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummaryFooter.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import styled from 'styled-components'; +import { ArrowRightOutlined } from '@ant-design/icons'; +import { Button } from 'antd'; +import { Assertion } from '../../../../../../../types.generated'; +import { StyledCheckOutlined, StyledCloseOutlined, StyledExclamationOutlined } from '../shared/styledComponents'; +import { getAssertionsSummary } from '../acrylUtils'; +import { ANTD_GRAY, REDESIGN_COLORS } from '../../../../constants'; + +const Container = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + +const StatusContainer = styled.div` + display: flex; + align-items: center; +`; + +const StatusText = styled.div` + color: ${ANTD_GRAY[8]}; + margin-left: 4px; +`; + +const ActionButton = styled(Button)` + color: ${REDESIGN_COLORS.BLUE}; +`; + +const StyledArrowRightOutlined = styled(ArrowRightOutlined)` + font-size: 8px; +`; + +type Props = { + assertions: Assertion[]; + passingText: string; + failingText: string; + errorText: string; + actionText?: string; + showAction?: boolean; +}; + +export const DataContractSummaryFooter = ({ + assertions, + actionText, + passingText, + errorText, + failingText, + showAction = true, +}: Props) => { + const summary = getAssertionsSummary(assertions); + const isFailing = summary.failing > 0; + const isPassing = summary.passing && summary.passing === summary.total; + const isErroring = summary.erroring > 0; + return ( + + + {(isFailing && ) || undefined} + {(isPassing && ) || undefined} + {(isErroring && !isFailing && ) || undefined} + + {(isFailing && failingText) || undefined} + {(isPassing && passingText) || undefined} + {(isErroring && errorText) || undefined} + + + {showAction && ( + + {actionText} + + + )} + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx index 5c009406b30d22..0a50213bea550c 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx @@ -1,15 +1,16 @@ import React, { useState } from 'react'; import styled from 'styled-components'; -// import { useGetDatasetContractQuery } from '../../../../../../../graphql/contract.generated'; -// import { DataContractState } from '../../../../../../../types.generated'; +import { useGetDatasetContractQuery } from '../../../../../../../graphql/contract.generated'; +import { DataContractState } from '../../../../../../../types.generated'; import { useEntityData } from '../../../../EntityContext'; -// import { DataContractSummary } from './DataContractSummary'; -// import { DataQualityContractSummary } from './DataQualityContractSummary'; -// import { SchemaContractSummary } from './SchemaContractSummary'; -// import { FreshnessContractSummary } from './FreshnessContractSummary'; -// import { DataContractBuilderModal } from './builder/DataContractBuilderModal'; -// import { createBuilderState } from './builder/utils'; -// import { getAssertionsSummary } from '../DatahubUtility'; +import { DataContractProposal } from './proposal/DataContractProposal'; +import { DataContractSummary } from './DataContractSummary'; +import { DataQualityContractSummary } from './DataQualityContractSummary'; +import { SchemaContractSummary } from './SchemaContractSummary'; +import { FreshnessContractSummary } from './FreshnessContractSummary'; +import { DataContractBuilderModal } from './builder/DataContractBuilderModal'; +import { createBuilderState } from './builder/utils'; +import { getAssertionsSummary } from '../acrylUtils'; const Container = styled.div` display: flex; @@ -27,92 +28,99 @@ const RightColumn = styled.div` * Component used for rendering the Data Contract Tab on the Assertions parent tab. */ export const DataContractTab = () => { - // const { urn, entityType } = useEntityData(); + const { urn, entityType } = useEntityData(); - // const { data, refetch } = useGetDatasetContractQuery({ - // variables: { - // urn, - // }, - // }); - // const [showContractBuilder, setShowContractBuilder] = useState(false); + const { data, refetch } = useGetDatasetContractQuery({ + variables: { + urn, + }, + }); + const [showContractBuilder, setShowContractBuilder] = useState(false); + console.log('data>>>>', data); - // const contract = data?.dataset?.contract; - // const schemaContracts = data?.dataset?.contract?.properties?.schema || []; - // const freshnessContracts = data?.dataset?.contract?.properties?.freshness || []; - // const dataQualityContracts = data?.dataset?.contract?.properties?.dataQuality || []; - // const schemaAssertions = schemaContracts.map((c) => c.assertion); - // const freshnessAssertions = freshnessContracts.map((c) => c.assertion); - // const dataQualityAssertions = dataQualityContracts.map((c) => c.assertion); - // const assertionsSummary = getAssertionsSummary([ - // ...schemaAssertions, - // ...freshnessAssertions, - // ...dataQualityAssertions, - // ] as any); - // const contractState = data?.dataset?.contract?.status?.state || DataContractState.Active; - // const hasFreshnessContract = freshnessContracts && freshnessContracts?.length; - // const hasSchemaContract = schemaContracts && schemaContracts?.length; - // const hasDataQualityContract = dataQualityContracts && dataQualityContracts?.length; - // const showLeftColumn = hasFreshnessContract || hasSchemaContract || undefined; + const contract = data?.dataset?.contract; + const schemaContracts = data?.dataset?.contract?.properties?.schema || []; + const freshnessContracts = data?.dataset?.contract?.properties?.freshness || []; + const dataQualityContracts = data?.dataset?.contract?.properties?.dataQuality || []; + const schemaAssertions = schemaContracts.map((c) => c.assertion); + const freshnessAssertions = freshnessContracts.map((c) => c.assertion); + const dataQualityAssertions = dataQualityContracts.map((c) => c.assertion); + const assertionsSummary = getAssertionsSummary([ + ...schemaAssertions, + ...freshnessAssertions, + ...dataQualityAssertions, + ] as any); + const contractState = data?.dataset?.contract?.status?.state || DataContractState.Active; + const hasFreshnessContract = freshnessContracts && freshnessContracts?.length; + const hasSchemaContract = schemaContracts && schemaContracts?.length; + const hasDataQualityContract = dataQualityContracts && dataQualityContracts?.length; + const showLeftColumn = hasFreshnessContract || hasSchemaContract || undefined; - // const onContractUpdate = () => { - // if (contract) { - // // Contract exists, just refetch. - // refetch(); - // } else { - // // no contract yet, wait for indxing, - // setTimeout(() => refetch(), 3000); - // } - // setShowContractBuilder(false); - // }; + const onContractUpdate = () => { + if (contract) { + // Contract exists, just refetch. + refetch(); + } else { + // no contract yet, wait for indxing, + setTimeout(() => refetch(), 3000); + } + setShowContractBuilder(false); + }; - // return ( - // <> - // {data?.dataset?.contract && ( - // <> - // setShowContractBuilder(true)} - // /> - // - // {showLeftColumn && ( - // - // {(hasFreshnessContract && ( - // - // )) || - // undefined} - // {(hasSchemaContract && ( - // - // )) || - // undefined} - // - // )} - // - // {(hasDataQualityContract && ( - // - // )) || - // undefined} - // - // - // - // )} - // {showContractBuilder && ( - // setShowContractBuilder(false)} - // onPropose={onContractUpdate} - // onSubmit={onContractUpdate} - // entityType={entityType} - // /> - // )} - // - // ); - return
DataContractTab
; + return ( + <> + {(data?.dataset?.contract && ( + <> + setShowContractBuilder(true)} + /> + + {showLeftColumn && ( + + {(hasFreshnessContract && ( + + )) || + undefined} + {(hasSchemaContract && ( + + )) || + undefined} + + )} + {/* + {(hasDataQualityContract && ( + + )) || + undefined} + */} + + + )) || ( + setShowContractBuilder(true)} + entityUrn={urn} + entityType={entityType} + /> + )} + {showContractBuilder && ( + setShowContractBuilder(false)} + onPropose={onContractUpdate} + onSubmit={onContractUpdate} + entityType={entityType} + /> + )} + + ); }; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataQualityContractSummary.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataQualityContractSummary.tsx new file mode 100644 index 00000000000000..fcf22e9e5d8ac8 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataQualityContractSummary.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Table } from 'antd'; +import { Assertion, DataQualityContract, DatasetAssertionInfo } from '../../../../../../../types.generated'; +import { ANTD_GRAY } from '../../../../constants'; +import { DataContractAssertionStatus } from './DataContractAssertionStatus'; +import { DataContractSummaryFooter } from './DataContractSummaryFooter'; +import { DatasetAssertionDescription } from '../DatasetAssertionDescription'; +import { FieldAssertionDescription } from '../FieldAssertionDescription'; +import { SqlAssertionDescription } from '../SqlAssertionDescription'; +// import { VolumeAssertionDescription } from '../VolumeAssertionDescription'; + +const TitleText = styled.div` + color: ${ANTD_GRAY[7]}; + margin-bottom: 20px; + letter-spacing: 1px; +`; + +const ColumnHeader = styled.div` + color: ${ANTD_GRAY[8]}; +`; + +const Container = styled.div` + padding: 28px; +`; + +const SummaryContainer = styled.div` + width: 100%; + display: flex; + align-items: center; +`; + +const StyledTable = styled(Table)` + width: 100%; + border-radius: 8px; + box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.1); +`; + +type Props = { + contracts: DataQualityContract[]; + showAction?: boolean; +}; + +export const DataQualityContractSummary = ({ contracts, showAction = false }: Props) => { + const assertions: Assertion[] = contracts?.map((contract) => contract.assertion); + + const columns = [ + { + title: () => ASSERTION, + render: (assertion: Assertion) => ( + <> + {assertion.info?.datasetAssertion && ( + + )} + {/* {assertion.info?.volumeAssertion && ( + + )} */} + {assertion.info?.fieldAssertion && ( + + )} + {assertion.info?.sqlAssertion && } + + ), + }, + { + title: () => STATUS, + render: (assertion: Assertion) => , + }, + ]; + + const data = (assertions || []).map((assertion) => ({ + ...assertion, + key: assertion.urn, + })); + + return ( + + DATA QUALITY + + ( + + )} + /> + + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/FreshnessContractSummary.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/FreshnessContractSummary.tsx new file mode 100644 index 00000000000000..efd0151b69bc2f --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/FreshnessContractSummary.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Divider } from 'antd'; +import { ClockCircleOutlined } from '@ant-design/icons'; +import { FreshnessContract } from '../../../../../../../types.generated'; +import { ANTD_GRAY } from '../../../../constants'; +import { DataContractSummaryFooter } from './DataContractSummaryFooter'; +import { FreshnessScheduleSummary } from './FreshnessScheduleSummary'; + +const Container = styled.div` + padding: 28px; +`; + +const TitleText = styled.div` + color: ${ANTD_GRAY[7]}; + margin-bottom: 20px; + letter-spacing: 1px; +`; + +const ThinDivider = styled(Divider)` + && { + padding: 0px; + margin: 0px; + } +`; + +const Header = styled.div` + color: ${ANTD_GRAY[8]}; + letter-spacing; 4px; + padding-top: 8px; + padding: 12px; + background-color: ${ANTD_GRAY[2]}; +`; + +const Body = styled.div` + padding: 12px; +`; + +const Footer = styled.div` + padding-top: 8px; + padding: 12px; + background-color: ${ANTD_GRAY[2]}; +`; + +const SummaryContainer = styled.div` + width: 100%; + border-radius: 8px; + box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.1); +`; + +type Props = { + contracts: FreshnessContract[]; + showAction?: boolean; +}; + +export const FreshnessContractSummary = ({ contracts, showAction = false }: Props) => { + // TODO: Support multiple per-asset contracts. + const firstContract = (contracts.length && contracts[0]) || undefined; + const assertionDefinition = firstContract?.assertion?.info?.freshnessAssertion?.schedule; + const evaluationSchedule = (firstContract?.assertion as any)?.monitor?.relationships[0]?.entity?.info + ?.assertionMonitor?.assertions[0]?.schedule; + + return ( + + FRESHNESS + +
+ + UPDATE FREQUENCY +
+ + {!assertionDefinition && <>No contract found :(} + + {assertionDefinition && ( + + )} + + + +
+ +
+
+
+ ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/FreshnessScheduleSummary.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/FreshnessScheduleSummary.tsx new file mode 100644 index 00000000000000..9dfb7216c58727 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/FreshnessScheduleSummary.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import cronstrue from 'cronstrue'; +// import { +// FreshnessAssertionSchedule, +// FreshnessAssertionScheduleType, +// CronSchedule, +// } from '../../../../../../../types.generated'; +import { capitalizeFirstLetter } from '../../../../../../shared/textUtil'; + +// type Props = { +// definition: FreshnessAssertionSchedule; +// evaluationSchedule?: CronSchedule; // When the assertion is run. +// }; + +export const FreshnessScheduleSummary = ({ definition, evaluationSchedule }: any) => { + const scheduleText = + definition.type === 'CRON' //FreshnessAssertionScheduleType.Cron + ? `${capitalizeFirstLetter(cronstrue.toString(definition.cron?.cron as string))}.` + : `In the past ${ + definition.fixedInterval?.multiple + } ${definition.fixedInterval?.unit.toLocaleLowerCase()}s${ + (evaluationSchedule && + `, as of ${cronstrue.toString(evaluationSchedule.cron as string).toLowerCase()}`) || + '' + }`; + + return <>{scheduleText}; +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/SchemaContractSummary.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/SchemaContractSummary.tsx new file mode 100644 index 00000000000000..7313a1064634c8 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/SchemaContractSummary.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Table } from 'antd'; +import { SchemaContract } from '../../../../../../../types.generated'; +import { ANTD_GRAY } from '../../../../constants'; +import { DataContractSummaryFooter } from './DataContractSummaryFooter'; + +const TitleText = styled.div` + color: ${ANTD_GRAY[7]}; + margin-bottom: 20px; + letter-spacing: 1px; +`; + +const ColumnHeader = styled.div` + color: ${ANTD_GRAY[8]}; +`; + +const Container = styled.div` + padding: 28px; +`; + +const SummaryContainer = styled.div` + width: 100%; + display: flex; + align-items: center; +`; + +const StyledTable = styled(Table)` + width: 100%; + border-radius: 8px; + box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.1); + height: 100%; +`; + +type Props = { + contracts: SchemaContract[]; + showAction?: boolean; +}; + +export const SchemaContractSummary = ({ contracts, showAction = false }: Props) => { + const firstContract = (contracts.length && contracts[0]) || undefined; + const schemaMetadata = firstContract?.assertion?.info?.schemaAssertion?.schema; + + const columns = [ + { + title: () => NAME, + render: (field) => <>{field.fieldPath}, + }, + { + title: () => TYPE, + render: (field) => <>{field.type}, + }, + ]; + + const data = (schemaMetadata?.fields || []).map((field) => ({ + ...field, + key: field.fieldPath, + })); + + return ( + + SCHEMA + + ( + + )} + /> + + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractAssertionGroupSelect.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractAssertionGroupSelect.tsx new file mode 100644 index 00000000000000..18c7200a12a4c0 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractAssertionGroupSelect.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Assertion } from '../../../../../../../../types.generated'; +import { ANTD_GRAY } from '../../../../../constants'; +import { AcrylDatasetAssertionsList } from '../../AcrylAssertionsList'; +import { DataContractCategoryType } from './types'; + +const Category = styled.div` + padding: 20px; + font-weight: bold; + font-size: 14px; + background-color: ${ANTD_GRAY[3]}; + border-radius: 4px; +`; + +const Hint = styled.span` + font-weight: normal; + font-size: 14px; + color: ${ANTD_GRAY[8]}; +`; + +type Props = { + category: DataContractCategoryType; + multiple?: boolean; + assertions: Assertion[]; + selectedUrns: string[]; + onSelect: (assertionUrn: string) => void; +}; + +/** + * Used for selecting the assertions that are part of a data contract + */ +export const DataContractAssertionGroupSelect = ({ + category, + assertions, + multiple = true, + selectedUrns, + onSelect, +}: Props) => { + return ( + <> + + {category} {!multiple && `(Choose 1)`} + + + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx new file mode 100644 index 00000000000000..b74f499f0030bb --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx @@ -0,0 +1,250 @@ +import React, { useState } from 'react'; +import { message, Button, Tooltip } from 'antd'; +import styled from 'styled-components'; +import lodash from 'lodash'; +import { + DataContract, + AssertionType, + // DataContractProposalOperationType, + // ActionRequestType, + EntityType, +} from '../../../../../../../../types.generated'; +import { DataContractBuilderState, DataContractCategoryType, DEFAULT_BUILDER_STATE } from './types'; +import { buildUpsertDataContractMutationVariables, buildProposeDataContractMutationVariables } from './utils'; +import { + // useProposeDataContractMutation, + useUpsertDataContractMutation, +} from '../../../../../../../../graphql/contract.generated'; +// import { useGetDatasetAssertionsWithMonitorsQuery } from '../../../../../../../../graphql/monitor.generated'; +import { + AssertionWithMonitorDetails, + // createAssertionGroups, + // tryExtractMonitorDetailsFromAssertionsWithMonitorsQuery, +} from '../../acrylUtils'; +import { DataContractAssertionGroupSelect } from './DataContractAssertionGroupSelect'; +import { ANTD_GRAY } from '../../../../../constants'; +import { DATA_QUALITY_ASSERTION_TYPES } from '../utils'; +import analytics, { EntityActionType, EventType } from '../../../../../../../analytics'; + +const AssertionsSection = styled.div` + border: 0.5px solid ${ANTD_GRAY[4]}; +`; + +const HeaderText = styled.div` + padding: 16px 20px; + color: ${ANTD_GRAY[7]}; + font-size: 16px; +`; + +const ActionContainer = styled.div` + display: flex; + justify-content: space-between; + margin-top: 16px; +`; + +const CancelButton = styled(Button)` + margin-left: 12px; +`; + +const ProposeButton = styled(Button)` + margin-right: 12px; +`; + +const SaveButton = styled(Button)` + margin-right: 20px; +`; + +type Props = { + entityUrn: string; + initialState?: DataContractBuilderState; + onSubmit?: (contract: DataContract) => void; + onPropose?: () => void; + onCancel?: () => void; + entityType?: EntityType; +}; + +/** + * This component is a modal used for constructing new Data Contracts + * + * In order to build a data contract, we simply list all dataset assertions and allow the user to choose. + */ +export const DataContractBuilder = ({ entityUrn, entityType, initialState, onSubmit, onPropose, onCancel }: Props) => { +// const isEdit = !!initialState; +// const [builderState, setBuilderState] = useState(initialState || DEFAULT_BUILDER_STATE); +// const [upsertDataContractMutation] = useUpsertDataContractMutation(); +// const [proposeDataContractMutation] = useProposeDataContractMutation(); + +// // note that for contracts, we do not allow the use of sibling node assertions, for clarity. +// const { data: assertionData } = useGetDatasetAssertionsWithMonitorsQuery({ +// variables: { urn: entityUrn }, +// fetchPolicy: 'cache-first', +// }); +// const assertionsWithMonitorsDetails: AssertionWithMonitorDetails[] = +// tryExtractMonitorDetailsFromAssertionsWithMonitorsQuery(assertionData) ?? []; +// const assertionGroups = createAssertionGroups(assertionsWithMonitorsDetails); +// const freshnessAssertions = +// assertionGroups.find((group) => group.type === AssertionType.Freshness)?.assertions || []; +// const schemaAssertions = assertionGroups.find((group) => group.type === AssertionType.DataSchema)?.assertions || []; +// const dataQualityAssertions = assertionGroups +// .filter((group) => DATA_QUALITY_ASSERTION_TYPES.has(group.type)) +// .flatMap((group) => group.assertions || []); + +// /** +// * Upserts the Data Contract for an entity +// */ +// const upsertDataContract = () => { +// return upsertDataContractMutation({ +// variables: buildUpsertDataContractMutationVariables(entityUrn, builderState), +// }) +// .then(({ data, errors }) => { +// if (!errors) { +// message.success({ +// content: isEdit ? `Edited Data Contract` : `Created Data Contract!`, +// duration: 3, +// }); +// onSubmit?.(data?.upsertDataContract as DataContract); +// } +// }) +// .catch(() => { +// message.destroy(); +// message.error({ content: 'Failed to create Data Contract! An unexpected error occurred' }); +// }); +// }; + +// /** +// * Proposes the upsert to the Data Contract for an entity +// */ +// const proposeUpsertDataContract = () => { +// return proposeDataContractMutation({ +// variables: buildProposeDataContractMutationVariables( +// DataContractProposalOperationType.Overwrite, +// entityUrn, +// builderState, +// ), +// }) +// .then(({ errors }) => { +// if (!errors) { +// analytics.event({ +// type: EventType.EntityActionEvent, +// actionType: EntityActionType.ProposalCreated, +// actionQualifier: ActionRequestType.DataContract, +// entityType, +// entityUrn, +// }); +// message.success({ +// content: `Proposed Data Contract!`, +// duration: 3, +// }); +// onPropose?.(); +// } +// }) +// .catch(() => { +// message.destroy(); +// message.error({ content: 'Failed to propose Data Contract! An unexpected error occurred' }); +// }); +// }; + +// const onSelectFreshnessAssertion = (assertionUrn: string) => { +// const selected = builderState.freshness?.assertionUrn === assertionUrn; +// if (selected) { +// setBuilderState({ +// ...builderState, +// freshness: undefined, +// }); +// } else { +// setBuilderState({ +// ...builderState, +// freshness: { assertionUrn }, +// }); +// } +// }; + +// const onSelectSchemaAssertion = (assertionUrn: string) => { +// const selected = builderState.schema?.assertionUrn === assertionUrn; +// if (selected) { +// setBuilderState({ +// ...builderState, +// schema: undefined, +// }); +// } else { +// setBuilderState({ +// ...builderState, +// schema: { assertionUrn }, +// }); +// } +// }; + +// const onSelectDataQualityAssertion = (assertionUrn: string) => { +// const selected = builderState.dataQuality?.some((c) => c.assertionUrn === assertionUrn); +// if (selected) { +// setBuilderState({ +// ...builderState, +// dataQuality: builderState.dataQuality?.filter((c) => c.assertionUrn !== assertionUrn), +// }); +// } else { +// setBuilderState({ +// ...builderState, +// dataQuality: [...(builderState.dataQuality || []), { assertionUrn }], +// }); +// } +// }; + +// const editDisabled = +// lodash.isEqual(builderState, initialState) || lodash.isEqual(builderState, DEFAULT_BUILDER_STATE); + +// const hasAssertions = freshnessAssertions.length || schemaAssertions.length || dataQualityAssertions.length; + +// return ( +// <> +// {(hasAssertions && Select the assertions that will make up your contract.) || ( +// Add a few assertions on this entity to create a data contract out of them. +// )} +// +// {(freshnessAssertions.length && ( +// +// )) || +// undefined} +// {(schemaAssertions.length && ( +// +// )) || +// undefined} +// {(dataQualityAssertions.length && ( +// c.assertionUrn) || []} +// onSelect={onSelectDataQualityAssertion} +// /> +// )) || +// undefined} +// +// +// Cancel +//
+// +// +// Propose +// +// +// +// Save +// +//
+//
+// +// ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilderModal.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilderModal.tsx new file mode 100644 index 00000000000000..9fe4540f13d50e --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilderModal.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { Modal, Typography } from 'antd'; +import { DataContract, EntityType } from '../../../../../../../../types.generated'; +import ClickOutside from '../../../../../../../shared/ClickOutside'; +import { DataContractBuilderState } from './types'; +// import { DataContractBuilder } from './DataContractBuilder'; + +const modalStyle = {}; +const modalBodyStyle = { + paddingRight: 0, + paddingLeft: 0, + paddingBottom: 20, + paddingTop: 0, + maxHeight: '70vh', + 'overflow-x': 'auto', +}; + +type Props = { + entityUrn: string; + initialState?: DataContractBuilderState; + onSubmit?: (contract: DataContract) => void; + onPropose?: () => void; + onCancel?: () => void; + entityType?: EntityType; +}; + +/** + * This component is a modal used for constructing new Data Contracts + */ +export const DataContractBuilderModal = ({ + entityUrn, + initialState, + onSubmit, + onPropose, + onCancel, + entityType, +}: Props) => { + const isEditing = initialState !== undefined; + const titleText = isEditing ? 'Edit Data Contract' : 'New Data Contract'; + + const modalClosePopup = () => { + Modal.confirm({ + title: 'Exit Editor', + content: `Are you sure you want to exit the editor? All changes will be lost`, + onOk() { + onCancel?.(); + }, + onCancel() {}, + okText: 'Yes', + maskClosable: true, + closable: true, + }); + }; + + return ( + + {titleText}} + style={modalStyle} + bodyStyle={modalBodyStyle} + visible + onCancel={onCancel} + > + {/* */} + + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/types.ts b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/types.ts new file mode 100644 index 00000000000000..d527837efd72ef --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/types.ts @@ -0,0 +1,37 @@ +/** + * The Data Contract Builder state + */ +export type DataContractBuilderState = { + /** + * The schema contract. In the UI, we only support defining a single schema contract. + */ + schema?: { + assertionUrn: string; + }; + + /** + * The freshness contract. In the UI, we only support defining a single freshness contract. + */ + freshness?: { + assertionUrn: string; + }; + + /** + * Data Quality contract. We cane define multiple data quality rules as part of the contract. + */ + dataQuality?: { + assertionUrn: string; + }[]; +}; + +export const DEFAULT_BUILDER_STATE = { + dataQuality: undefined, + schema: undefined, + freshness: undefined, +}; + +export enum DataContractCategoryType { + FRESHNESS = 'Freshness', + SCHEMA = 'Schema', + DATA_QUALITY = 'Data Quality', +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/utils.ts b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/utils.ts new file mode 100644 index 00000000000000..281fa4b3855a91 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/utils.ts @@ -0,0 +1,139 @@ +import { DataContractProposalOperationType, DataContract } from '../../../../../../../../types.generated'; +import { DataContractBuilderState, DataContractCategoryType } from './types'; + +/** + * Creates a builder state instance from a Data Contract object. + */ +export const createBuilderState = (contract?: DataContract | null): DataContractBuilderState | undefined => { + if (contract) { + return { + schema: + (contract?.properties?.schema?.length && { + assertionUrn: contract?.properties?.schema[0].assertion.urn, + }) || + undefined, + freshness: + (contract?.properties?.freshness?.length && { + assertionUrn: contract?.properties?.freshness[0].assertion.urn, + }) || + undefined, + dataQuality: + contract?.properties?.dataQuality?.map((c) => ({ assertionUrn: c.assertion.urn })) || undefined, + }; + } + return undefined; +}; + +/** + * Constructs the input variables required for upserting a data contract using graphql + */ +export const buildUpsertDataContractMutationVariables = (entityUrn: string, state: DataContractBuilderState) => { + return { + input: { + entityUrn, + freshness: (state.freshness && [state.freshness]) || [], + schema: (state.schema && [state.schema]) || [], + dataQuality: state.dataQuality || [], + }, + }; +}; + +/** + * Constructs the input variables required for proposing a data contract using graphql + */ +export const buildProposeDataContractMutationVariables = ( + operationType: DataContractProposalOperationType, + entityUrn: string, + state: DataContractBuilderState, +) => { + return { + input: { + operationType, + entityUrn, + freshness: (state.freshness && [state.freshness]) || [], + schema: (state.schema && [state.schema]) || [], + dataQuality: state.dataQuality || [], + }, + }; +}; + +/** + * Constructs the input variables required for removing an assertion from a data contract using graphql. + */ +export const buildRemoveAssertionFromContractMutationVariables = ( + entityUrn: string, + assertionUrn: string, + contract?: DataContract, +) => { + return { + input: { + entityUrn, + freshness: contract?.properties?.freshness + ?.filter((c) => c.assertion.urn !== assertionUrn) + ?.map((c) => ({ + assertionUrn: c.assertion.urn, + })), + schema: contract?.properties?.schema + ?.filter((c) => c.assertion.urn !== assertionUrn) + ?.map((c) => ({ + assertionUrn: c.assertion.urn, + })), + dataQuality: contract?.properties?.dataQuality + ?.filter((c) => c.assertion.urn !== assertionUrn) + ?.map((c) => ({ + assertionUrn: c.assertion.urn, + })), + }, + }; +}; + +/** + * Constructs the input variables required for adding an assertion to a data contract using graphql. + */ +export const buildAddAssertionToContractMutationVariables = ( + category: DataContractCategoryType, + entityUrn: string, + assertionUrn: string, + contract?: DataContract, +) => { + const baseInput = { + entityUrn, + freshness: contract?.properties?.freshness?.map((c) => ({ + assertionUrn: c.assertion.urn, + })), + schema: contract?.properties?.schema?.map((c) => ({ + assertionUrn: c.assertion.urn, + })), + dataQuality: contract?.properties?.dataQuality?.map((c) => ({ + assertionUrn: c.assertion.urn, + })), + }; + + switch (category) { + case DataContractCategoryType.SCHEMA: + // Replace schema assertion. We only support 1 schema assertion at a time (currently). + return { + input: { + ...baseInput, + schema: [{ assertionUrn }], + }, + }; + case DataContractCategoryType.FRESHNESS: + // Replace freshness assertion. We only support 1 freshness assertion at a time (currently). + return { + input: { + ...baseInput, + freshness: [{ assertionUrn }], + }, + }; + case DataContractCategoryType.DATA_QUALITY: + return { + input: { + ...baseInput, + dataQuality: [...(baseInput.dataQuality || []), { assertionUrn }], + }, + }; + default: + throw new Error(`Unrecognized category type ${category} provided.`); + } +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposal.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposal.tsx new file mode 100644 index 00000000000000..31985fc111900b --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposal.tsx @@ -0,0 +1,274 @@ +import React, { useEffect } from 'react'; +import styled from 'styled-components'; +import { Button, message, Modal, Typography } from 'antd'; +import { + CheckOutlined, + CloseOutlined, + EyeOutlined, + InfoCircleOutlined, + PlusOutlined, + StopOutlined, +} from '@ant-design/icons'; +import { useEntityData } from '../../../../../EntityContext'; +// import { useGetContractProposalsQuery } from '../../../../../../../../graphql/contract.generated'; +// import { +// ActionRequestStatus, +// ActionRequestType, +// DataContractProposalParams, +// EntityType, +// } from '../../../../../../../../types.generated'; +import { DataContractProposalDescription } from './DataContractProposalDescription'; +// import { +// useAcceptProposalMutation, +// useRejectProposalMutation, +// } from '../../../../../../../../graphql/actionRequest.generated'; +import { ANTD_GRAY } from '../../../../../constants'; +import { FAILURE_COLOR_HEX } from '../../../../Incident/incidentUtils'; +import { FreshnessContractSummary } from '../FreshnessContractSummary'; +import { SchemaContractSummary } from '../SchemaContractSummary'; +import { DataQualityContractSummary } from '../DataQualityContractSummary'; +import analytics, { EntityActionType, EventType } from '../../../../../../../analytics'; + +const Container = styled.div``; + +const Summary = styled.div` + width: 100%; + padding-left: 40px; + padding-top: 20px; + padding-bottom: 20px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid ${ANTD_GRAY[4.5]}; + box-shadow: 0px 2px 6px 0px #0000000d; +`; + +const SummaryDescription = styled.div` + display: flex; + align-items: center; +`; + +const SummaryMessage = styled.div` + display: inline-block; + margin-left: 20px; + max-width: 350px; +`; + +const SummaryTitle = styled(Typography.Title)` + && { + padding-bottom: 0px; + margin-bottom: 4px; + } +`; + +const Actions = styled.div` + margin: 12px; + margin-right: 20px; +`; + +const ApproveButton = styled(Button)` + margin-right: 12px; + background-color: ${(props) => props.theme.styles['primary-color']}; + border-color: ${(props) => props.theme.styles['primary-color']}; + color: white; + letter-spacing: 2px; +`; + +const CreateButton = styled(Button)` + margin-right: 12px; + border-color: ${(props) => props.theme.styles['primary-color']}; + color: ${(props) => props.theme.styles['primary-color']}; + letter-spacing: 2px; + &&:hover { + color: white; + background-color: ${(props) => props.theme.styles['primary-color']}; + border-color: ${(props) => props.theme.styles['primary-color']}; + } +`; + +const DenyButton = styled(Button)` + color: ${FAILURE_COLOR_HEX}; + border-color: ${FAILURE_COLOR_HEX}; + letter-spacing: 2px; + &&:hover { + color: white; + background-color: ${FAILURE_COLOR_HEX}; + border-color: ${FAILURE_COLOR_HEX}; + } + margin-right: 12px; +`; + +const StyledInfoCircleOutlined = styled(InfoCircleOutlined)` + margin-left: 4px; + font-size: 10px; + color: ${ANTD_GRAY[7]}; +`; + +const StyledEyeOutlined = styled(EyeOutlined)` + font-size: 24px; + color: ${ANTD_GRAY[7]}; +`; + +// type Props = { +// showContractBuilder: () => void; +// refetch: () => void; +// entityUrn: string; +// entityType?: EntityType; +// }; + +/** + * Displaying a Data Contract proposal for an entity. + */ + +export const DataContractProposal = ({ showContractBuilder, refetch, entityUrn, entityType }: any) => { + const { urn } = useEntityData(); + // const [acceptProposalMutation] = useAcceptProposalMutation(); + // const [rejectProposalMutation] = useRejectProposalMutation(); + // const { data, refetch: contractRefetch } = useGetContractProposalsQuery({ + // variables: { + // urn, + // }, + // }); + const data: any = { + listActionRequests: { + total: 0, + actionRequests: [], + __typename: 'ListActionRequestsResult', + }, + }; + const contractRefetch = () => null; + + useEffect(() => { + contractRefetch(); + }, [contractRefetch]); + + const acceptProposal = (actionRequestUrn) => { + Modal.confirm({ + title: 'Accept Proposed Contract', + content: 'Are you sure you want to accept this proposal? New assertions will be created for this dataset.', + okText: 'Yes', + onOk() { + // acceptProposalMutation({ variables: { urn: actionRequestUrn } }) + // .then(() => { + // analytics.event({ + // type: EventType.EntityActionEvent, + // actionType: 'ProposalAccepted', //EntityActionType.ProposalAccepted, + // actionQualifier: 'DATA_CONTRACT', //ActionRequestType.DataContract, + // entityType, + // entityUrn, + // }); + // setTimeout(() => refetch(), 3000); + // message.success('Successfully accepted the proposal!'); + // }) + // .catch((err) => { + // console.log(err); + // message.error('Failed to accept proposal. :('); + // }); + }, + }); + }; + + const rejectProposal = (actionRequestUrn) => { + Modal.confirm({ + title: 'Reject Proposed Contract', + content: + 'Are you sure you want to reject this proposal? Proposals will no longer be created for this dataset.', + okText: 'Yes', + onOk() { + // rejectProposalMutation({ variables: { urn: actionRequestUrn } }) + // .then(() => { + // analytics.event({ + // type: EventType.EntityActionEvent, + // actionType: 'ProposalRejected', //EntityActionType.ProposalRejected, + // actionQualifier: 'DATA_CONTRACT', // ActionRequestType.DataContract, + // entityType, + // entityUrn, + // }); + // contractRefetch(); + // setTimeout(() => refetch(), 3000); + // message.success('Rejected the proposal.'); + // }) + // .catch((err) => { + // console.log(err); + // message.error('Failed to reject proposal. :('); + // }); + }, + }); + }; + + // Extract the Contract which is being proposed. Note that this only goes to the current user if they are able to approve. + const hasContractProposal = + ((data?.listActionRequests?.total || 0) > 0 && data?.listActionRequests?.actionRequests?.length) || undefined; + const contractActionRequest = hasContractProposal && data?.listActionRequests?.actionRequests[0]; + const actionRequestStatus = contractActionRequest && contractActionRequest.status; + const actionRequestParams = contractActionRequest && contractActionRequest.params; + const actionRequestUrn = contractActionRequest && contractActionRequest.urn; + const contractProposal = actionRequestParams && actionRequestParams?.dataContractProposal; + const isActiveProposal = contractProposal && actionRequestStatus === 'PENDING'; //ActionRequestStatus.Pending; + + return ( + + + + {(isActiveProposal && ) || ( + + )} + + {(isActiveProposal && ( + <> + + There is a contract proposal pending review + + + + )) || ( + + No contract found +
+ + A contract does not yet exist for this dataset + +
+
+ )} +
+
+ {(isActiveProposal && ( + + acceptProposal(actionRequestUrn as any)}> + + ACCEPT + + rejectProposal(actionRequestUrn as any)}> + + REJECT + + + + CREATE + + + )) || ( + + + + CREATE + + + )} +
+ {isActiveProposal && contractProposal && contractProposal?.freshness && ( + + )} + {isActiveProposal && contractProposal && contractProposal?.schema && ( + + )} + {isActiveProposal && contractProposal && contractProposal?.dataQuality && ( + + )} +
+ ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposalDescription.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposalDescription.tsx new file mode 100644 index 00000000000000..52dea63959cba3 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposalDescription.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { Typography } from 'antd'; +import { DataContractProposalParams } from '../../../../../../../../types.generated'; +import { DBT_URN } from '../../../../../../../ingest/source/builder/constants'; + +/** + * Returns true if the contract is implemented externally, + * e.g. within a source system like Dbt. + * + * This differs on a per-system basis. + */ +const isExternalContract = (urn) => { + if (urn.includes(DBT_URN)) { + return { isExternal: true, platformName: 'dbt' }; + } + return { isExternal: false, platformName: undefined }; +}; + +/** + * Returns the number of assertions that are involved in a particular Data Contract Proposal. + */ +const getAssertionCount = (proposal: DataContractProposalParams) => { + return (proposal.freshness?.length || 0) + (proposal.dataQuality?.length || 0) + (proposal.schema?.length || 0); +}; + +type Props = { + urn: string; + proposal: DataContractProposalParams; +}; + +/** + * A description for a Data Contract Proposal + */ +export const DataContractProposalDescription = ({ urn, proposal }: Props) => { + const { isExternal, platformName } = isExternalContract(urn); + const assertionCount = getAssertionCount(proposal); + return ( + + Proposal to create new Data Contract from {assertionCount} assertions. + {isExternal && ( + <> + This contract contains external Assertions which will be provisioned in {platformName} + + )} + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposalModal.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposalModal.tsx new file mode 100644 index 00000000000000..2f5472e532317e --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposalModal.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Modal, Button } from 'antd'; +import { DataContractProposalParams } from '../../../../../../../../types.generated'; +import { FreshnessContractSummary } from '../FreshnessContractSummary'; +import { SchemaContractSummary } from '../SchemaContractSummary'; +import { DataQualityContractSummary } from '../DataQualityContractSummary'; +import { ANTD_GRAY } from '../../../../../constants'; + +const NoAssertions = styled.div` + padding: 20px; + font-size: 16px; + color: ${ANTD_GRAY[7]}; +`; + +const ActionButton = styled(Button)``; + +type Props = { + proposal: DataContractProposalParams; + showActions?: boolean; + onClose: () => void; + onApprove?: () => void; + onDeny?: () => void; +}; + +/** + * Displaying a Data Contract Proposal for an entity. + */ +export const DataContractProposalModal = ({ proposal, showActions = true, onClose, onApprove, onDeny }: Props) => { + const hasAssertions = proposal.freshness?.length || proposal.schema?.length || proposal.dataQuality?.length; + + return ( + + {!hasAssertions && Proposal to remove all assertions from Data Contract} + {(proposal.freshness?.length && ) || undefined} + {(proposal.schema?.length && ) || undefined} + {(proposal.dataQuality?.length && ) || + undefined} + {showActions && ( + <> + Approve + Deny + + )} + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/utils.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/utils.tsx new file mode 100644 index 00000000000000..c541ceec6f8db0 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/utils.tsx @@ -0,0 +1,109 @@ +import React from 'react'; +import { + CheckOutlined, + ClockCircleOutlined, + CloseOutlined, + ExclamationCircleFilled, + StopOutlined, +} from '@ant-design/icons'; +import { Assertion, AssertionType, DataContract, DataContractState } from '../../../../../../../types.generated'; +import { ANTD_GRAY } from '../../../../constants'; +import { FAILURE_COLOR_HEX, SUCCESS_COLOR_HEX, WARNING_COLOR_HEX } from '../acrylUtils'; +import { DataContractCategoryType } from './builder/types'; +import { AssertionStatusSummary } from '../acrylTypes'; + +export const getContractSummaryIcon = (state: DataContractState, summary: AssertionStatusSummary) => { + if (state === DataContractState.Pending) { + return ; + } + if (summary.total === 0) { + return ; + } + if (summary.passing === summary.total) { + return ; + } + if (summary.failing > 0) { + return ; + } + if (summary.erroring > 0) { + return ; + } + return ; +}; + +export const getContractSummaryTitle = (state: DataContractState, summary: AssertionStatusSummary) => { + if (state === DataContractState.Pending) { + return 'This contract is pending implementation'; + } + if (summary.total === 0) { + return 'This contract has not yet been validated'; + } + if (summary.passing === summary.total) { + return 'This dataset is meeting its contract'; + } + if (summary.failing > 0) { + return 'This dataset is not meeting its contract'; + } + if (summary.erroring > 0) { + return 'Unable to determine contract status'; + } + return 'This contract has not yet been validated'; +}; + +export const getContractSummaryMessage = (state: DataContractState, summary: AssertionStatusSummary) => { + if (state === DataContractState.Pending) { + return 'This may take some time. Come back later!'; + } + if (summary.total === 0) { + return 'No contract assertions have been run yet'; + } + if (summary.passing === summary.total) { + return 'All contract assertions are passing'; + } + if (summary.failing > 0) { + return 'Some contract assertions are failing'; + } + if (summary.erroring > 0) { + return 'Some contract assertions are completing with errors'; + } + return 'No contract assertions have been run yet'; +}; + +/** + * Returns true if a given assertion is part of a given contract, false otherwise. + */ +export const isAssertionPartOfContract = (assertion: Assertion, contract: DataContract) => { + if (contract.properties?.dataQuality?.some((c) => c.assertion.urn === assertion.urn)) { + return true; + } + if (contract.properties?.schema?.some((c) => c.assertion.urn === assertion.urn)) { + return true; + } + if (contract.properties?.freshness?.some((c) => c.assertion.urn === assertion.urn)) { + return true; + } + return false; +}; + +/** + * Retrieves the high level contract category - schema, freshness, or data quality - given an assertion + */ +export const getDataContractCategoryFromAssertion = (assertion: Assertion) => { + if ( + assertion.info?.type === AssertionType.Dataset || + assertion.info?.type === AssertionType.Volume || + assertion.info?.type === AssertionType.Field || + assertion.info?.type === AssertionType.Sql + ) { + return DataContractCategoryType.DATA_QUALITY; + } + if (assertion.info?.type === AssertionType.Freshness) { + return DataContractCategoryType.FRESHNESS; + } + if (assertion.info?.type === AssertionType.DataSchema) { + return DataContractCategoryType.SCHEMA; + } + return DataContractCategoryType.DATA_QUALITY; +}; + +export const DATA_QUALITY_ASSERTION_TYPES = new Set([AssertionType.Volume, AssertionType.Sql, AssertionType.Field]); diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/fieldDescriptionUtils.ts b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/fieldDescriptionUtils.ts new file mode 100644 index 00000000000000..9c10e03daa6ed6 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/fieldDescriptionUtils.ts @@ -0,0 +1,149 @@ +import { + AssertionStdOperator, + AssertionStdParameters, + // FieldAssertionInfo, + // FieldAssertionType, + // FieldMetricType, + // FieldTransformType, +} from '../../../../../../types.generated'; +import { formatNumberWithoutAbbreviation } from '../../../../../shared/formatNumber'; +import { parseMaybeStringAsFloatOrDefault } from '../../../../../shared/numberUtil'; +// import { ASSERTION_OPERATOR_TO_DESCRIPTION } from './assertion/profile/summary/shared/constants'; + +const SUPPORTED_OPERATORS_FOR_FIELD_DESCRIPTION = [ + AssertionStdOperator.EqualTo, + AssertionStdOperator.Null, + AssertionStdOperator.NotNull, + AssertionStdOperator.NotEqualTo, + AssertionStdOperator.NotIn, + AssertionStdOperator.RegexMatch, + AssertionStdOperator.GreaterThan, + AssertionStdOperator.LessThan, + AssertionStdOperator.GreaterThanOrEqualTo, + AssertionStdOperator.LessThanOrEqualTo, + AssertionStdOperator.In, + AssertionStdOperator.Between, + AssertionStdOperator.Contain, + AssertionStdOperator.IsTrue, + AssertionStdOperator.IsFalse, +]; +// const getAssertionStdOperator = (operator: AssertionStdOperator) => { +// if (!ASSERTION_OPERATOR_TO_DESCRIPTION[operator] || !SUPPORTED_OPERATORS_FOR_FIELD_DESCRIPTION.includes(operator)) { +// throw new Error(`Unknown operator ${operator}`); +// } +// return ASSERTION_OPERATOR_TO_DESCRIPTION[operator]?.toLowerCase(); +// }; + +export const getFieldMetricTypeReadableLabel = (metric: FieldMetricType) => { + switch (metric) { + case FieldMetricType.NullCount: + return 'Null count'; + case FieldMetricType.NullPercentage: + return 'Null percentage'; + case FieldMetricType.UniqueCount: + return 'Unique count'; + case FieldMetricType.UniquePercentage: + return 'Unique percentage'; + case FieldMetricType.MaxLength: + return 'Max length'; + case FieldMetricType.MinLength: + return 'Min length'; + case FieldMetricType.EmptyCount: + return 'Empty count'; + case FieldMetricType.EmptyPercentage: + return 'Empty percentage'; + case FieldMetricType.Max: + return 'Max'; + case FieldMetricType.Min: + return 'Min'; + case FieldMetricType.Mean: + return 'Average'; + case FieldMetricType.Median: + return 'Median'; + case FieldMetricType.NegativeCount: + return 'Negative count'; + case FieldMetricType.NegativePercentage: + return 'Negative percentage'; + case FieldMetricType.Stddev: + return 'Standard deviation'; + case FieldMetricType.ZeroCount: + return 'Zero count'; + case FieldMetricType.ZeroPercentage: + return 'Zero percentage'; + default: + throw new Error(`Unknown field metric type ${metric}`); + } +}; + +const getFieldTransformType = (transform: FieldTransformType) => { + switch (transform) { + case FieldTransformType.Length: + return 'Length'; + default: + throw new Error(`Unknown field transform type ${transform}`); + } +}; + +const getAssertionStdParameters = (parameters: AssertionStdParameters) => { + if (parameters.value) { + return formatNumberWithoutAbbreviation(parseMaybeStringAsFloatOrDefault(parameters.value.value, parameters.value.value)); + } + if (parameters.minValue && parameters.maxValue) { + return `${formatNumberWithoutAbbreviation(parseMaybeStringAsFloatOrDefault(parameters.minValue.value, parameters.minValue.value))} and ${formatNumberWithoutAbbreviation(parseMaybeStringAsFloatOrDefault(parameters.maxValue.value, parameters.maxValue.value))}`; + } + return ''; +}; + +export const getFieldDescription = (assertionInfo: FieldAssertionInfo) => { + const { type, fieldValuesAssertion, fieldMetricAssertion } = assertionInfo; + switch (type) { + case FieldAssertionType.FieldValues: + return fieldValuesAssertion?.field?.path; + case FieldAssertionType.FieldMetric: + return fieldMetricAssertion?.field?.path; + default: + throw new Error(`Unknown field assertion type ${type}`); + } +}; + +export const getFieldOperatorDescription = (assertionInfo: FieldAssertionInfo) => { + const { type, fieldValuesAssertion, fieldMetricAssertion } = assertionInfo; + switch (type) { + case FieldAssertionType.FieldValues: + if (!fieldValuesAssertion?.operator) return ''; + return getAssertionStdOperator(fieldValuesAssertion.operator); + case FieldAssertionType.FieldMetric: + if (!fieldMetricAssertion?.operator) return ''; + return getAssertionStdOperator(fieldMetricAssertion.operator); + default: + throw new Error(`Unknown field assertion type ${type}`); + } +}; + +export const getFieldTransformDescription = (assertionInfo: FieldAssertionInfo) => { + const { type, fieldValuesAssertion, fieldMetricAssertion } = assertionInfo; + switch (type) { + case FieldAssertionType.FieldValues: + if (!fieldValuesAssertion?.transform?.type) return ''; + return getFieldTransformType(fieldValuesAssertion.transform.type); + case FieldAssertionType.FieldMetric: + if (!fieldMetricAssertion?.metric) return ''; + return getFieldMetricTypeReadableLabel(fieldMetricAssertion.metric); + default: + throw new Error(`Unknown field assertion type ${type}`); + } +}; + +export const getFieldParametersDescription = (assertionInfo: FieldAssertionInfo) => { + const { type, fieldValuesAssertion, fieldMetricAssertion } = assertionInfo; + switch (type) { + case FieldAssertionType.FieldValues: + if (!fieldValuesAssertion?.parameters) return ''; + return getAssertionStdParameters(fieldValuesAssertion.parameters); + case FieldAssertionType.FieldMetric: + if (!fieldMetricAssertion?.parameters) return ''; + return getAssertionStdParameters(fieldMetricAssertion.parameters); + default: + throw new Error(`Unknown field assertion type ${type}`); + } +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/shared/styledComponents.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/shared/styledComponents.tsx new file mode 100644 index 00000000000000..a43e8ad841eeeb --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/shared/styledComponents.tsx @@ -0,0 +1,32 @@ +import styled from 'styled-components'; +import { CheckOutlined, ClockCircleOutlined, CloseOutlined, ExclamationCircleOutlined } from '@ant-design/icons'; +import { ANTD_GRAY } from '../../../../constants'; +import { FAILURE_COLOR_HEX, SUCCESS_COLOR_HEX, WARNING_COLOR_HEX } from '../acrylUtils'; + +export const StyledCheckOutlined = styled(CheckOutlined)` + color: ${SUCCESS_COLOR_HEX}; + font-size: 16px; + margin-right: 4px; + margin-left: 4px; +`; + +export const StyledCloseOutlined = styled(CloseOutlined)` + color: ${FAILURE_COLOR_HEX}; + font-size: 16px; + margin-right: 4px; + margin-left: 4px; +`; + +export const StyledExclamationOutlined = styled(ExclamationCircleOutlined)` + color: ${WARNING_COLOR_HEX}; + font-size: 16px; + margin-right: 4px; + margin-left: 4px; +`; + +export const StyledClockCircleOutlined = styled(ClockCircleOutlined)` + color: ${ANTD_GRAY[6]}; + font-size: 16px; + margin-right: 4px; + margin-left: 4px; +`; diff --git a/datahub-web-react/src/app/shared/numberUtil.ts b/datahub-web-react/src/app/shared/numberUtil.ts new file mode 100644 index 00000000000000..6b6b76b04d0d14 --- /dev/null +++ b/datahub-web-react/src/app/shared/numberUtil.ts @@ -0,0 +1,24 @@ + +export function parseMaybeStringAsFloatOrDefault(str: any, fallback?: T): number | T | undefined { + const parsedValue = typeof str === 'string' ? parseFloat(str) : str; + return typeof parsedValue === 'number' && !Number.isNaN(parsedValue) ? parsedValue : fallback; +} + + +export function parseJsonArrayOrDefault(str: any, fallback: T[] = []): T[] | undefined { + // Check if the input is a string and try to parse it. + if (typeof str === 'string') { + try { + const parsedValue = JSON.parse(str); + // Check if the parsed value is an array before returning. + if (Array.isArray(parsedValue)) { + return parsedValue; + } + } catch (e) { + // If parsing throws, log the error (optional) and proceed to return fallback. + console.error("Failed to parse JSON:", e); + } + } + // Return fallback if the above conditions fail. + return fallback; +} \ No newline at end of file diff --git a/datahub-web-react/src/graphql/actionRequest.graphql b/datahub-web-react/src/graphql/actionRequest.graphql new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/datahub-web-react/src/graphql/contract.graphql b/datahub-web-react/src/graphql/contract.graphql new file mode 100644 index 00000000000000..2a17cbed1ee942 --- /dev/null +++ b/datahub-web-react/src/graphql/contract.graphql @@ -0,0 +1,54 @@ +fragment contractAssertionParameters on AssertionStdParameters { + value { + value + type + } + minValue { + value + type + } + maxValue { + value + type + } +} + +fragment dataContractDetails on DataContract { + urn + type + properties { + freshness { + assertion { + ...assertionDetailsWithMonitors + } + } + schema { + assertion { + ...assertionDetailsWithMonitors + } + } + dataQuality { + assertion { + ...assertionDetailsWithMonitors + } + } + } + status { + state + } +} + +query getDatasetContract($urn: String!) { + dataset(urn: $urn) { + contract { + ...dataContractDetails + } + } +} + +mutation upsertDataContract($input: UpsertDataContractInput!) { + upsertDataContract(input: $input) { + ...dataContractDetails + } +} + diff --git a/datahub-web-react/src/graphql/monitor.graphql b/datahub-web-react/src/graphql/monitor.graphql new file mode 100644 index 00000000000000..6e0326f3f65c15 --- /dev/null +++ b/datahub-web-react/src/graphql/monitor.graphql @@ -0,0 +1,11 @@ +fragment assertionDetailsWithMonitors on Assertion { + ...assertionDetails + runEvents(status: COMPLETE, limit: 1) { + total + failed + succeeded + runEvents { + ...assertionRunEventDetails + } + } +} diff --git a/datahub-web-react/yarn.lock b/datahub-web-react/yarn.lock index aad6d84c6bd93d..642398a3e77d15 100644 --- a/datahub-web-react/yarn.lock +++ b/datahub-web-react/yarn.lock @@ -825,6 +825,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.23.9": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.6.tgz#5b76eb89ad45e2e4a0a8db54c456251469a3358e" + integrity sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.18.10", "@babel/template@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" @@ -1755,6 +1762,13 @@ resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.15.tgz#490f3dea5327c892f063496a0219c48301da0fa0" integrity sha512-xFcS0LpdF0Q1qJrrNsYUv9PU+ovvhCEPTOMw2jcpEFtl3CA87dLpvztORR5oE2UBFjWF7qLQLOwboQU1+xC7Cw== +"@mui/icons-material@^5.15.2": + version "5.15.19" + resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.15.19.tgz#0602da80d814af662812659eab891e435ec0d5c0" + integrity sha512-RsEiRxA5azN9b8gI7JRqekkgvxQUlitoBOtZglflb8cUDyP12/cP4gRwhb44Ea1/zwwGGjAj66ZJpGHhKfibNA== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/material@^5.8.5": version "5.10.15" resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.10.15.tgz#37345f5a3d71c662703af7b5be0cca229b2a1416" @@ -4645,15 +4659,10 @@ camelize@^1.0.0: resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= -caniuse-lite@^1.0.30001503: - version "1.0.30001508" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001508.tgz#4461bbc895c692a96da399639cc1e146e7302a33" - integrity sha512-sdQZOJdmt3GJs1UMNpCCCyeuS2IEGLXnHyAo9yIO5JJDjbjoVRij4M1qep6P6gFpptD1PqIYgzM+gwJbOi92mw== - -caniuse-lite@^1.0.30001541: - version "1.0.30001559" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001559.tgz#95a982440d3d314c471db68d02664fb7536c5a30" - integrity sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA== +caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001541: + version "1.0.30001625" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz" + integrity sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w== capital-case@^1.0.4: version "1.0.4" @@ -5057,6 +5066,13 @@ create-react-class@^15.6.2: loose-envify "^1.3.1" object-assign "^4.1.1" +cron-parser@^4.8.1: + version "4.9.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.9.0.tgz#0340694af3e46a0894978c6f52a6dbb5c0f11ad5" + integrity sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q== + dependencies: + luxon "^3.2.1" + cronstrue@^1.122.0: version "1.122.0" resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-1.122.0.tgz#bd6838077b476d28f61d381398b47b8c3912a126" @@ -7554,6 +7570,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +luxon@^3.2.1: + version "3.4.4" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.4.tgz#cf20dc27dc532ba41a169c43fdcc0063601577af" + integrity sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA== + lz-string@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" @@ -9391,6 +9412,11 @@ regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" From 6785fc9730a01ed563244748e5a8b3e93bf518d3 Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Fri, 31 May 2024 20:26:10 +0530 Subject: [PATCH 04/26] feat(data-contract): add modal files to create contract --- .../Validations/AcrylAssertionsList.tsx | 50 +++ .../Validations/AcrylAssertionsTable.tsx | 234 +++++++++++ .../tabs/Dataset/Validations/acrylUtils.tsx | 78 ++-- .../contract/builder/DataContractBuilder.tsx | 363 +++++++++--------- .../builder/DataContractBuilderModal.tsx | 6 +- 5 files changed, 508 insertions(+), 223 deletions(-) create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/AcrylAssertionsList.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/AcrylAssertionsTable.tsx diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/AcrylAssertionsList.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/AcrylAssertionsList.tsx new file mode 100644 index 00000000000000..e7f96699891aea --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/AcrylAssertionsList.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Assertion, DataContract } from '../../../../../../types.generated'; +import { AcrylAssertionsTable } from './AcrylAssertionsTable'; + +type Props = { + assertions: Array; + contract?: DataContract; + showMenu?: boolean; + showSelect?: boolean; + selectedUrns?: string[]; + canEditAssertions: boolean; + canEditMonitors: boolean; + canEditSqlAssertions: boolean; + onSelect?: (assertionUrn: string) => void; + refetch?: () => void; +}; + +/** + * Acryl-specific list of assertions displaying their most recent run status, their human-readable + * description, and platform. + * + * Currently this component supports rendering Dataset Assertions only. + */ +export const AcrylDatasetAssertionsList = ({ + assertions, + contract, + showMenu, + showSelect, + selectedUrns, + canEditAssertions, + canEditMonitors, + canEditSqlAssertions, + onSelect, + refetch, +}: Props) => { + return ( + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/AcrylAssertionsTable.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/AcrylAssertionsTable.tsx new file mode 100644 index 00000000000000..b1cfd184d2aaa9 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/AcrylAssertionsTable.tsx @@ -0,0 +1,234 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { Checkbox, Empty, Table, TableProps } from 'antd'; +import { Assertion, AssertionRunStatus, AssertionType, DataContract, Entity } from '../../../../../../types.generated'; +import { useEntityData } from '../../../../../entity/shared/EntityContext'; +// import { ActionsColumn, DetailsColumn } from './AcrylAssertionsTableColumns'; +// import { AssertionProfileDrawer } from './assertion/profile/AssertionProfileDrawer'; +import { ANTD_GRAY } from '../../../constants'; +// import { useOpenAssertionDetailModal } from './assertion/builder/hooks'; + +type StyledTableProps = { + showSelect?: boolean; +} & TableProps; + +export const StyledTable = styled(Table)` + ${(props) => !props.showSelect && `margin-left: -50px;`} + max-width: none; + overflow: inherit; + height: inherit; + &&& .ant-table-thead .ant-table-cell { + font-weight: 600; + font-size: 12px; + color: ${ANTD_GRAY[8]}; + } + && + .ant-table-thead + > tr + > th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not( + [colspan] + )::before { + border: 1px solid ${ANTD_GRAY[4]}; + } + && { + .ant-table-tbody > tr > td { + border: none; + ${(props) => props.showSelect && `padding: 16px 20px;`} + } + } + &&& .ant-table-cell { + background-color: transparent; + } + &&& .acryl-assertions-table-row { + cursor: pointer; + background-color: ${ANTD_GRAY[2]}; + :hover { + background-color: ${ANTD_GRAY[3]}; + } + } + &&& .acryl-selected-assertions-table-row { + background-color: ${ANTD_GRAY[4]}; + } +`; + +const DetailsColumnWrapper = styled.div` + display: flex; + align-items: center; + justify-content: left; +`; + +const AssertionSelectCheckbox = styled(Checkbox)` + margin-right: 12px; +`; + +type Props = { + assertions: Array; + contract?: DataContract; + showMenu?: boolean; + showSelect?: boolean; + selectedUrns?: string[]; + canEditAssertions: boolean; + canEditMonitors: boolean; + canEditSqlAssertions: boolean; + onSelect?: (assertionUrn: string) => void; + refetch?: () => void; +}; + +/** + * Acryl-specific list of assertions displaying their most recent run status, their human-readable + * description, and platform. + * + * Currently this component supports rendering Dataset Assertions only. + */ +export const AcrylAssertionsTable = ({ + assertions, + contract, + showMenu = true, + showSelect = false, + selectedUrns, + canEditAssertions, + canEditMonitors, + canEditSqlAssertions, + onSelect, + refetch, +}: Props) => { + const { entityData } = useEntityData(); + const [focusAssertionUrn, setFocusAssertionUrn] = useState(null); + + const focusedAssertion = assertions.find((assertion) => assertion.urn === focusAssertionUrn); + const canEditFocusAssertion = focusedAssertion + ? (focusedAssertion?.info?.type === AssertionType.Sql && canEditSqlAssertions) || canEditAssertions + : false; + const canEditFocusMonitor = focusedAssertion ? canEditMonitors : false; + + if (focusAssertionUrn && !focusedAssertion) { + setFocusAssertionUrn(null); + } + + // useOpenAssertionDetailModal(setFocusAssertionUrn); + + const assertionsTableData = assertions.map((assertion) => ({ + urn: assertion.urn, + type: assertion.info?.type, + platform: assertion.platform, + datasetAssertionInfo: assertion.info?.datasetAssertion, + freshnessAssertionInfo: assertion.info?.freshnessAssertion, + lastEvaluation: + assertion.runEvents?.runEvents?.length && + assertion.runEvents.runEvents[0].status === AssertionRunStatus.Complete && + assertion.runEvents.runEvents[0], + lastEvaluationTimeMs: + assertion.runEvents?.runEvents?.length && assertion.runEvents.runEvents[0].timestampMillis, + lastEvaluationResult: + assertion.runEvents?.runEvents?.length && + assertion.runEvents.runEvents[0].status === AssertionRunStatus.Complete && + assertion.runEvents.runEvents[0].result?.type, + lastEvaluationUrl: + assertion.runEvents?.runEvents?.length && + assertion.runEvents.runEvents[0].status === AssertionRunStatus.Complete && + assertion.runEvents.runEvents[0].result?.externalUrl, + assertion, + monitor: + (assertion as any).monitor?.relationships?.length && (assertion as any).monitor?.relationships[0].entity, + })); + + const assertionsTableCols = [ + { + title: '', + dataIndex: '', + key: '', + render: (_, record: any) => { + const selected = selectedUrns?.some((selectedUrn) => selectedUrn === record.urn); + return ( + + {showSelect && ( + e.stopPropagation()} + onChange={() => onSelect?.(record.urn as string)} + /> + )} + {/* setFocusAssertionUrn(record.urn)} + /> */} + + ); + }, + }, + { + title: '', + dataIndex: '', + key: '', + render: (_, record: any) => { + // TODO: Add permission for editing contract. + const isSqlAssertion = record.type === AssertionType.Sql; + return ( + <> + {/* {(showMenu && ( + + )) || + undefined} */} + + ); + }, + }, + ]; + + return ( + <> + { + return ( + (record.urn === focusAssertionUrn && 'acryl-selected-assertions-table-row') || + 'acryl-assertions-table-row' + ); + }} + columns={assertionsTableCols} + dataSource={assertionsTableData} + rowKey="urn" + locale={{ + emptyText: , + }} + onRow={(record) => { + return { + onClick: (_) => { + if (showSelect) { + onSelect?.(record.urn as string); + } else { + setFocusAssertionUrn(record.urn); + } + }, + }; + }} + showHeader={false} + pagination={false} + /> + {/* {focusAssertionUrn && ( + setFocusAssertionUrn(null)} + refetch={refetch} + /> + )} */} + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylUtils.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylUtils.tsx index d730f290cb3237..4dbf53a05b20a9 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylUtils.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylUtils.tsx @@ -195,17 +195,17 @@ export type AssertionWithMonitorDetails = Assertion & { monitors?: any[]; // should almost always have 0-1 items }; -// export const tryExtractMonitorDetailsFromAssertionsWithMonitorsQuery = ( -// queryData?: GetDatasetAssertionsWithMonitorsQuery, -// ): AssertionWithMonitorDetails[] | undefined => { -// return queryData?.dataset?.assertions?.assertions?.map((assertion) => ({ -// ...(assertion as Assertion), -// monitors: -// assertion.monitor?.relationships -// ?.filter((r) => r.entity?.__typename === 'Monitor') -// .map((r) => r.entity as MonitorDetailsFragment) ?? [], -// })); -// }; +export const tryExtractMonitorDetailsFromAssertionsWithMonitorsQuery = ( + queryData?: any, //GetDatasetAssertionsWithMonitorsQuery, +): AssertionWithMonitorDetails[] | undefined => { + return queryData?.dataset?.assertions?.assertions?.map((assertion) => ({ + ...(assertion as Assertion), + monitors: + assertion.monitor?.relationships?.filter((r) => r.entity?.__typename === 'Monitor').map((r) => r.entity) ?? + [], + // .map((r) => r.entity as MonitorDetailsFragment) ?? [], + })); +}; /** * Returns a status summary for the assertions associated with a Dataset. @@ -273,35 +273,35 @@ export const getLegacyAssertionsSummary = (assertions: Assertion[]) => { // * // * @param assertions The assertions to group // */ -// export const createAssertionGroups = (assertions: Array): AssertionGroup[] => { -// // Pre-sort the list of assertions based on which has been most recently executed. -// assertions.sort(sortAssertions); - -// const typeToAssertions = new Map(); -// assertions -// .filter((assertion) => assertion.info?.type) -// .forEach((assertion) => { -// const groupType = assertion.info?.type; -// const groupedAssertions = typeToAssertions.get(groupType) || []; -// groupedAssertions.push(assertion); -// typeToAssertions.set(groupType, groupedAssertions); -// }); - -// // Now, create summary for each type and build the AssertionGroup object -// const assertionGroups: AssertionGroup[] = []; -// typeToAssertions.forEach((groupedAssertions, type) => { -// const newGroup: AssertionGroup = { -// name: getAssertionGroupName(type), -// icon: getAssertionGroupTypeIcon(type), -// assertions: groupedAssertions, -// summary: getAssertionsSummary(groupedAssertions), -// type, -// }; -// assertionGroups.push(newGroup); -// }); +export const createAssertionGroups = (assertions: Array): AssertionGroup[] => { + // Pre-sort the list of assertions based on which has been most recently executed. + assertions.sort(sortAssertions); + + const typeToAssertions = new Map(); + assertions + .filter((assertion) => assertion.info?.type) + .forEach((assertion) => { + const groupType = assertion.info?.type; + const groupedAssertions = typeToAssertions.get(groupType) || []; + groupedAssertions.push(assertion); + typeToAssertions.set(groupType, groupedAssertions); + }); + + // Now, create summary for each type and build the AssertionGroup object + const assertionGroups: AssertionGroup[] = []; + typeToAssertions.forEach((groupedAssertions, type) => { + const newGroup: AssertionGroup = { + name: getAssertionGroupName(type), + icon: getAssertionGroupTypeIcon(type), + assertions: groupedAssertions, + summary: getAssertionsSummary(groupedAssertions), + type, + }; + assertionGroups.push(newGroup); + }); -// return assertionGroups; -// }; + return assertionGroups; +}; // TODO: Make this the default inside DatasetAssertionsSummary.tsx. export const getAssertionGroupSummaryIcon = (summary: AssertionStatusSummary) => { diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx index b74f499f0030bb..839f67c337518d 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx @@ -18,13 +18,13 @@ import { // import { useGetDatasetAssertionsWithMonitorsQuery } from '../../../../../../../../graphql/monitor.generated'; import { AssertionWithMonitorDetails, - // createAssertionGroups, - // tryExtractMonitorDetailsFromAssertionsWithMonitorsQuery, + createAssertionGroups, + tryExtractMonitorDetailsFromAssertionsWithMonitorsQuery, } from '../../acrylUtils'; import { DataContractAssertionGroupSelect } from './DataContractAssertionGroupSelect'; import { ANTD_GRAY } from '../../../../../constants'; import { DATA_QUALITY_ASSERTION_TYPES } from '../utils'; -import analytics, { EntityActionType, EventType } from '../../../../../../../analytics'; +// import analytics, { EntityActionType, EventType } from '../../../../../../../analytics'; const AssertionsSection = styled.div` border: 0.5px solid ${ANTD_GRAY[4]}; @@ -69,182 +69,183 @@ type Props = { * In order to build a data contract, we simply list all dataset assertions and allow the user to choose. */ export const DataContractBuilder = ({ entityUrn, entityType, initialState, onSubmit, onPropose, onCancel }: Props) => { -// const isEdit = !!initialState; -// const [builderState, setBuilderState] = useState(initialState || DEFAULT_BUILDER_STATE); -// const [upsertDataContractMutation] = useUpsertDataContractMutation(); -// const [proposeDataContractMutation] = useProposeDataContractMutation(); - -// // note that for contracts, we do not allow the use of sibling node assertions, for clarity. -// const { data: assertionData } = useGetDatasetAssertionsWithMonitorsQuery({ -// variables: { urn: entityUrn }, -// fetchPolicy: 'cache-first', -// }); -// const assertionsWithMonitorsDetails: AssertionWithMonitorDetails[] = -// tryExtractMonitorDetailsFromAssertionsWithMonitorsQuery(assertionData) ?? []; -// const assertionGroups = createAssertionGroups(assertionsWithMonitorsDetails); -// const freshnessAssertions = -// assertionGroups.find((group) => group.type === AssertionType.Freshness)?.assertions || []; -// const schemaAssertions = assertionGroups.find((group) => group.type === AssertionType.DataSchema)?.assertions || []; -// const dataQualityAssertions = assertionGroups -// .filter((group) => DATA_QUALITY_ASSERTION_TYPES.has(group.type)) -// .flatMap((group) => group.assertions || []); - -// /** -// * Upserts the Data Contract for an entity -// */ -// const upsertDataContract = () => { -// return upsertDataContractMutation({ -// variables: buildUpsertDataContractMutationVariables(entityUrn, builderState), -// }) -// .then(({ data, errors }) => { -// if (!errors) { -// message.success({ -// content: isEdit ? `Edited Data Contract` : `Created Data Contract!`, -// duration: 3, -// }); -// onSubmit?.(data?.upsertDataContract as DataContract); -// } -// }) -// .catch(() => { -// message.destroy(); -// message.error({ content: 'Failed to create Data Contract! An unexpected error occurred' }); -// }); -// }; - -// /** -// * Proposes the upsert to the Data Contract for an entity -// */ -// const proposeUpsertDataContract = () => { -// return proposeDataContractMutation({ -// variables: buildProposeDataContractMutationVariables( -// DataContractProposalOperationType.Overwrite, -// entityUrn, -// builderState, -// ), -// }) -// .then(({ errors }) => { -// if (!errors) { -// analytics.event({ -// type: EventType.EntityActionEvent, -// actionType: EntityActionType.ProposalCreated, -// actionQualifier: ActionRequestType.DataContract, -// entityType, -// entityUrn, -// }); -// message.success({ -// content: `Proposed Data Contract!`, -// duration: 3, -// }); -// onPropose?.(); -// } -// }) -// .catch(() => { -// message.destroy(); -// message.error({ content: 'Failed to propose Data Contract! An unexpected error occurred' }); -// }); -// }; - -// const onSelectFreshnessAssertion = (assertionUrn: string) => { -// const selected = builderState.freshness?.assertionUrn === assertionUrn; -// if (selected) { -// setBuilderState({ -// ...builderState, -// freshness: undefined, -// }); -// } else { -// setBuilderState({ -// ...builderState, -// freshness: { assertionUrn }, -// }); -// } -// }; - -// const onSelectSchemaAssertion = (assertionUrn: string) => { -// const selected = builderState.schema?.assertionUrn === assertionUrn; -// if (selected) { -// setBuilderState({ -// ...builderState, -// schema: undefined, -// }); -// } else { -// setBuilderState({ -// ...builderState, -// schema: { assertionUrn }, -// }); -// } -// }; - -// const onSelectDataQualityAssertion = (assertionUrn: string) => { -// const selected = builderState.dataQuality?.some((c) => c.assertionUrn === assertionUrn); -// if (selected) { -// setBuilderState({ -// ...builderState, -// dataQuality: builderState.dataQuality?.filter((c) => c.assertionUrn !== assertionUrn), -// }); -// } else { -// setBuilderState({ -// ...builderState, -// dataQuality: [...(builderState.dataQuality || []), { assertionUrn }], -// }); -// } -// }; - -// const editDisabled = -// lodash.isEqual(builderState, initialState) || lodash.isEqual(builderState, DEFAULT_BUILDER_STATE); - -// const hasAssertions = freshnessAssertions.length || schemaAssertions.length || dataQualityAssertions.length; - -// return ( -// <> -// {(hasAssertions && Select the assertions that will make up your contract.) || ( -// Add a few assertions on this entity to create a data contract out of them. -// )} -// -// {(freshnessAssertions.length && ( -// -// )) || -// undefined} -// {(schemaAssertions.length && ( -// -// )) || -// undefined} -// {(dataQualityAssertions.length && ( -// c.assertionUrn) || []} -// onSelect={onSelectDataQualityAssertion} -// /> -// )) || -// undefined} -// -// -// Cancel -//
-// -// -// Propose -// -// -// -// Save -// -//
-//
-// -// ); + const isEdit = !!initialState; + const [builderState, setBuilderState] = useState(initialState || DEFAULT_BUILDER_STATE); + const [upsertDataContractMutation] = useUpsertDataContractMutation(); + // const [proposeDataContractMutation] = useProposeDataContractMutation(); + + // note that for contracts, we do not allow the use of sibling node assertions, for clarity. + // const { data: assertionData } = useGetDatasetAssertionsWithMonitorsQuery({ + // variables: { urn: entityUrn }, + // fetchPolicy: 'cache-first', + // }); + const assertionData = []; + const assertionsWithMonitorsDetails: AssertionWithMonitorDetails[] = + tryExtractMonitorDetailsFromAssertionsWithMonitorsQuery(assertionData) ?? []; + const assertionGroups = createAssertionGroups(assertionsWithMonitorsDetails); + const freshnessAssertions = + assertionGroups.find((group) => group.type === AssertionType.Freshness)?.assertions || []; + const schemaAssertions = assertionGroups.find((group) => group.type === AssertionType.DataSchema)?.assertions || []; + const dataQualityAssertions = assertionGroups + .filter((group) => DATA_QUALITY_ASSERTION_TYPES.has(group.type)) + .flatMap((group) => group.assertions || []); + + /** + * Upserts the Data Contract for an entity + */ + const upsertDataContract = () => { + return upsertDataContractMutation({ + variables: buildUpsertDataContractMutationVariables(entityUrn, builderState), + }) + .then(({ data, errors }) => { + if (!errors) { + message.success({ + content: isEdit ? `Edited Data Contract` : `Created Data Contract!`, + duration: 3, + }); + onSubmit?.(data?.upsertDataContract as DataContract); + } + }) + .catch(() => { + message.destroy(); + message.error({ content: 'Failed to create Data Contract! An unexpected error occurred' }); + }); + }; + + /** + * Proposes the upsert to the Data Contract for an entity + */ + const proposeUpsertDataContract = () => { + // return proposeDataContractMutation({ + // variables: buildProposeDataContractMutationVariables( + // DataContractProposalOperationType.Overwrite, + // entityUrn, + // builderState, + // ), + // }) + // .then(({ errors }) => { + // if (!errors) { + // analytics.event({ + // type: EventType.EntityActionEvent, + // actionType: EntityActionType.ProposalCreated, + // actionQualifier: ActionRequestType.DataContract, + // entityType, + // entityUrn, + // }); + // message.success({ + // content: `Proposed Data Contract!`, + // duration: 3, + // }); + // onPropose?.(); + // } + // }) + // .catch(() => { + // message.destroy(); + // message.error({ content: 'Failed to propose Data Contract! An unexpected error occurred' }); + // }); + }; + + const onSelectFreshnessAssertion = (assertionUrn: string) => { + const selected = builderState.freshness?.assertionUrn === assertionUrn; + if (selected) { + setBuilderState({ + ...builderState, + freshness: undefined, + }); + } else { + setBuilderState({ + ...builderState, + freshness: { assertionUrn }, + }); + } + }; + + const onSelectSchemaAssertion = (assertionUrn: string) => { + const selected = builderState.schema?.assertionUrn === assertionUrn; + if (selected) { + setBuilderState({ + ...builderState, + schema: undefined, + }); + } else { + setBuilderState({ + ...builderState, + schema: { assertionUrn }, + }); + } + }; + + const onSelectDataQualityAssertion = (assertionUrn: string) => { + const selected = builderState.dataQuality?.some((c) => c.assertionUrn === assertionUrn); + if (selected) { + setBuilderState({ + ...builderState, + dataQuality: builderState.dataQuality?.filter((c) => c.assertionUrn !== assertionUrn), + }); + } else { + setBuilderState({ + ...builderState, + dataQuality: [...(builderState.dataQuality || []), { assertionUrn }], + }); + } + }; + + const editDisabled = + lodash.isEqual(builderState, initialState) || lodash.isEqual(builderState, DEFAULT_BUILDER_STATE); + + const hasAssertions = freshnessAssertions.length || schemaAssertions.length || dataQualityAssertions.length; + + return ( + <> + {(hasAssertions && Select the assertions that will make up your contract.) || ( + Add a few assertions on this entity to create a data contract out of them. + )} + + {(freshnessAssertions.length && ( + + )) || + undefined} + {(schemaAssertions.length && ( + + )) || + undefined} + {(dataQualityAssertions.length && ( + c.assertionUrn) || []} + onSelect={onSelectDataQualityAssertion} + /> + )) || + undefined} + + + Cancel +
+ + + Propose + + + + Save + +
+
+ + ); }; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilderModal.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilderModal.tsx index 9fe4540f13d50e..a788e8848c5841 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilderModal.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilderModal.tsx @@ -3,7 +3,7 @@ import { Modal, Typography } from 'antd'; import { DataContract, EntityType } from '../../../../../../../../types.generated'; import ClickOutside from '../../../../../../../shared/ClickOutside'; import { DataContractBuilderState } from './types'; -// import { DataContractBuilder } from './DataContractBuilder'; +import { DataContractBuilder } from './DataContractBuilder'; const modalStyle = {}; const modalBodyStyle = { @@ -64,14 +64,14 @@ export const DataContractBuilderModal = ({ visible onCancel={onCancel} > - {/* */} + /> ); From e89e384f182eaa7d798b93c716fb9eaf2c6c0860 Mon Sep 17 00:00:00 2001 From: jayacryl <159848059+jayacryl@users.noreply.github.com> Date: Fri, 31 May 2024 23:07:38 -0400 Subject: [PATCH 05/26] spotless --- .../linkedin/metadata/authorization/PoliciesConfig.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java index 10af24f5876ef1..d20b9bc8f01beb 100644 --- a/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java +++ b/metadata-utils/src/main/java/com/linkedin/metadata/authorization/PoliciesConfig.java @@ -346,10 +346,10 @@ public class PoliciesConfig { "The ability to edit the Queries for a Dataset."); public static final Privilege EDIT_ENTITY_DATA_CONTRACT_PRIVILEGE = - Privilege.of( - "EDIT_ENTITY_DATA_CONTRACT", - "Edit Data Contract", - "The ability to edit the Data Contract for an entity."); + Privilege.of( + "EDIT_ENTITY_DATA_CONTRACT", + "Edit Data Contract", + "The ability to edit the Data Contract for an entity."); // Tag Privileges public static final Privilege EDIT_TAG_COLOR_PRIVILEGE = From 10c7568db22caabd4e72a913ddf9b155d0b7d37a Mon Sep 17 00:00:00 2001 From: jayacryl <159848059+jayacryl@users.noreply.github.com> Date: Fri, 31 May 2024 23:13:50 -0400 Subject: [PATCH 06/26] wired contracts to resolver --- .../datahub/graphql/GmsGraphQLEngine.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index 5315a444d07b7b..f000c0f43df324 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -54,6 +54,7 @@ import com.linkedin.datahub.graphql.generated.DataJob; import com.linkedin.datahub.graphql.generated.DataJobInputOutput; import com.linkedin.datahub.graphql.generated.DataPlatformInstance; +import com.linkedin.datahub.graphql.generated.DataQualityContract; import com.linkedin.datahub.graphql.generated.Dataset; import com.linkedin.datahub.graphql.generated.DatasetStatsSummary; import com.linkedin.datahub.graphql.generated.Domain; @@ -64,6 +65,7 @@ import com.linkedin.datahub.graphql.generated.EntityRelationshipLegacy; import com.linkedin.datahub.graphql.generated.ForeignKeyConstraint; import com.linkedin.datahub.graphql.generated.FormActorAssignment; +import com.linkedin.datahub.graphql.generated.FreshnessContract; import com.linkedin.datahub.graphql.generated.GetRootGlossaryNodesResult; import com.linkedin.datahub.graphql.generated.GetRootGlossaryTermsResult; import com.linkedin.datahub.graphql.generated.GlossaryNode; @@ -102,6 +104,7 @@ import com.linkedin.datahub.graphql.generated.QuickFilter; import com.linkedin.datahub.graphql.generated.RecommendationContent; import com.linkedin.datahub.graphql.generated.ResolvedAuditStamp; +import com.linkedin.datahub.graphql.generated.SchemaContract; import com.linkedin.datahub.graphql.generated.SchemaField; import com.linkedin.datahub.graphql.generated.SchemaFieldEntity; import com.linkedin.datahub.graphql.generated.SearchAcrossLineageResult; @@ -136,6 +139,8 @@ import com.linkedin.datahub.graphql.resolvers.container.ParentContainersResolver; import com.linkedin.datahub.graphql.resolvers.dashboard.DashboardStatsSummaryResolver; import com.linkedin.datahub.graphql.resolvers.dashboard.DashboardUsageStatsResolver; +import com.linkedin.datahub.graphql.resolvers.datacontract.EntityDataContractResolver; +import com.linkedin.datahub.graphql.resolvers.datacontract.UpsertDataContractResolver; import com.linkedin.datahub.graphql.resolvers.dataproduct.BatchSetDataProductResolver; import com.linkedin.datahub.graphql.resolvers.dataproduct.CreateDataProductResolver; import com.linkedin.datahub.graphql.resolvers.dataproduct.DeleteDataProductResolver; @@ -741,6 +746,7 @@ public void configureRuntimeWiring(final RuntimeWiring.Builder builder) { configureDomainResolvers(builder); configureDataProductResolvers(builder); configureAssertionResolvers(builder); + configureContractResolvers(builder); configurePolicyResolvers(builder); configureDataProcessInstanceResolvers(builder); configureVersionedDatasetResolvers(builder); @@ -2709,6 +2715,59 @@ private void configureAssertionResolvers(final RuntimeWiring.Builder builder) { "aspects", new WeaklyTypedAspectsResolver(entityClient, entityRegistry))); } + private void configureContractResolvers(final RuntimeWiring.Builder builder) { + builder.type( + "Dataset", + typeWiring -> + typeWiring.dataFetcher( + "contract", new EntityDataContractResolver(this.entityClient, this.graphClient))); + builder.type( + "FreshnessContract", + typeWiring -> + typeWiring.dataFetcher( + "assertion", + new LoadableTypeResolver<>( + getAssertionType(), + (env) -> { + final FreshnessContract contract = env.getSource(); + return contract.getAssertion() != null + ? contract.getAssertion().getUrn() + : null; + }))); + builder.type( + "DataQualityContract", + typeWiring -> + typeWiring.dataFetcher( + "assertion", + new LoadableTypeResolver<>( + getAssertionType(), + (env) -> { + final DataQualityContract contract = env.getSource(); + return contract.getAssertion() != null + ? contract.getAssertion().getUrn() + : null; + }))); + builder.type( + "SchemaContract", + typeWiring -> + typeWiring.dataFetcher( + "assertion", + new LoadableTypeResolver<>( + getAssertionType(), + (env) -> { + final SchemaContract contract = env.getSource(); + return contract.getAssertion() != null + ? contract.getAssertion().getUrn() + : null; + }))); + builder.type( + "Mutation", + typeWiring -> + typeWiring.dataFetcher( + "upsertDataContract", + new UpsertDataContractResolver(this.entityClient, this.graphClient))); + } + private void configurePolicyResolvers(final RuntimeWiring.Builder builder) { // Register resolvers for "resolvedUsers" and "resolvedGroups" field of the Policy type. builder.type( From 292a936286534fb4fae62f10717cc2ac3bd3356c Mon Sep 17 00:00:00 2001 From: jayacryl <159848059+jayacryl@users.noreply.github.com> Date: Sat, 1 Jun 2024 08:48:37 -0400 Subject: [PATCH 07/26] added schema file --- .../src/main/java/com/linkedin/datahub/graphql/Constants.java | 1 + .../java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/Constants.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/Constants.java index 0924dbc0c0a6d9..abe269679dd751 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/Constants.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/Constants.java @@ -21,6 +21,7 @@ private Constants() {} public static final String PROPERTIES_SCHEMA_FILE = "properties.graphql"; public static final String FORMS_SCHEMA_FILE = "forms.graphql"; public static final String INCIDENTS_SCHEMA_FILE = "incident.graphql"; + public static final String CONTRACTS_SCHEMA_FILE = "contract.graphql"; public static final String CONNECTIONS_SCHEMA_FILE = "connection.graphql"; public static final String BROWSE_PATH_DELIMITER = "/"; public static final String BROWSE_PATH_V2_DELIMITER = "␟"; diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index f000c0f43df324..f017cbb067d8a1 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -820,7 +820,8 @@ public GraphQLEngine.Builder builder() { .addSchema(fileBasedSchema(PROPERTIES_SCHEMA_FILE)) .addSchema(fileBasedSchema(FORMS_SCHEMA_FILE)) .addSchema(fileBasedSchema(CONNECTIONS_SCHEMA_FILE)) - .addSchema(fileBasedSchema(INCIDENTS_SCHEMA_FILE)); + .addSchema(fileBasedSchema(INCIDENTS_SCHEMA_FILE)) + .addSchema(fileBasedSchema(CONTRACTS_SCHEMA_FILE)); for (GmsGraphQLPlugin plugin : this.graphQLPlugins) { List pluginSchemaFiles = plugin.getSchemaFiles(); From 3a6b3ef264b8b45a01f04e55756ab994827fdf3c Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Sun, 2 Jun 2024 09:28:31 +0530 Subject: [PATCH 08/26] feat(ui/data-contract): remove acryl file and use datahub component for contract --- .../Validations/AcrylAssertionsList.tsx | 50 ---- .../Validations/AcrylAssertionsTable.tsx | 234 ------------------ .../DataContractAssertionGroupSelect.tsx | 18 +- .../proposal/DataContractProposal.tsx | 156 ++---------- .../src/graphql/actionRequest.graphql | 0 .../src/graphql/contract.graphql | 7 +- 6 files changed, 27 insertions(+), 438 deletions(-) delete mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/AcrylAssertionsList.tsx delete mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/AcrylAssertionsTable.tsx delete mode 100644 datahub-web-react/src/graphql/actionRequest.graphql diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/AcrylAssertionsList.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/AcrylAssertionsList.tsx deleted file mode 100644 index e7f96699891aea..00000000000000 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/AcrylAssertionsList.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import { Assertion, DataContract } from '../../../../../../types.generated'; -import { AcrylAssertionsTable } from './AcrylAssertionsTable'; - -type Props = { - assertions: Array; - contract?: DataContract; - showMenu?: boolean; - showSelect?: boolean; - selectedUrns?: string[]; - canEditAssertions: boolean; - canEditMonitors: boolean; - canEditSqlAssertions: boolean; - onSelect?: (assertionUrn: string) => void; - refetch?: () => void; -}; - -/** - * Acryl-specific list of assertions displaying their most recent run status, their human-readable - * description, and platform. - * - * Currently this component supports rendering Dataset Assertions only. - */ -export const AcrylDatasetAssertionsList = ({ - assertions, - contract, - showMenu, - showSelect, - selectedUrns, - canEditAssertions, - canEditMonitors, - canEditSqlAssertions, - onSelect, - refetch, -}: Props) => { - return ( - - ); -}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/AcrylAssertionsTable.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/AcrylAssertionsTable.tsx deleted file mode 100644 index b1cfd184d2aaa9..00000000000000 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/AcrylAssertionsTable.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import React, { useState } from 'react'; -import styled from 'styled-components'; -import { Checkbox, Empty, Table, TableProps } from 'antd'; -import { Assertion, AssertionRunStatus, AssertionType, DataContract, Entity } from '../../../../../../types.generated'; -import { useEntityData } from '../../../../../entity/shared/EntityContext'; -// import { ActionsColumn, DetailsColumn } from './AcrylAssertionsTableColumns'; -// import { AssertionProfileDrawer } from './assertion/profile/AssertionProfileDrawer'; -import { ANTD_GRAY } from '../../../constants'; -// import { useOpenAssertionDetailModal } from './assertion/builder/hooks'; - -type StyledTableProps = { - showSelect?: boolean; -} & TableProps; - -export const StyledTable = styled(Table)` - ${(props) => !props.showSelect && `margin-left: -50px;`} - max-width: none; - overflow: inherit; - height: inherit; - &&& .ant-table-thead .ant-table-cell { - font-weight: 600; - font-size: 12px; - color: ${ANTD_GRAY[8]}; - } - && - .ant-table-thead - > tr - > th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not( - [colspan] - )::before { - border: 1px solid ${ANTD_GRAY[4]}; - } - && { - .ant-table-tbody > tr > td { - border: none; - ${(props) => props.showSelect && `padding: 16px 20px;`} - } - } - &&& .ant-table-cell { - background-color: transparent; - } - &&& .acryl-assertions-table-row { - cursor: pointer; - background-color: ${ANTD_GRAY[2]}; - :hover { - background-color: ${ANTD_GRAY[3]}; - } - } - &&& .acryl-selected-assertions-table-row { - background-color: ${ANTD_GRAY[4]}; - } -`; - -const DetailsColumnWrapper = styled.div` - display: flex; - align-items: center; - justify-content: left; -`; - -const AssertionSelectCheckbox = styled(Checkbox)` - margin-right: 12px; -`; - -type Props = { - assertions: Array; - contract?: DataContract; - showMenu?: boolean; - showSelect?: boolean; - selectedUrns?: string[]; - canEditAssertions: boolean; - canEditMonitors: boolean; - canEditSqlAssertions: boolean; - onSelect?: (assertionUrn: string) => void; - refetch?: () => void; -}; - -/** - * Acryl-specific list of assertions displaying their most recent run status, their human-readable - * description, and platform. - * - * Currently this component supports rendering Dataset Assertions only. - */ -export const AcrylAssertionsTable = ({ - assertions, - contract, - showMenu = true, - showSelect = false, - selectedUrns, - canEditAssertions, - canEditMonitors, - canEditSqlAssertions, - onSelect, - refetch, -}: Props) => { - const { entityData } = useEntityData(); - const [focusAssertionUrn, setFocusAssertionUrn] = useState(null); - - const focusedAssertion = assertions.find((assertion) => assertion.urn === focusAssertionUrn); - const canEditFocusAssertion = focusedAssertion - ? (focusedAssertion?.info?.type === AssertionType.Sql && canEditSqlAssertions) || canEditAssertions - : false; - const canEditFocusMonitor = focusedAssertion ? canEditMonitors : false; - - if (focusAssertionUrn && !focusedAssertion) { - setFocusAssertionUrn(null); - } - - // useOpenAssertionDetailModal(setFocusAssertionUrn); - - const assertionsTableData = assertions.map((assertion) => ({ - urn: assertion.urn, - type: assertion.info?.type, - platform: assertion.platform, - datasetAssertionInfo: assertion.info?.datasetAssertion, - freshnessAssertionInfo: assertion.info?.freshnessAssertion, - lastEvaluation: - assertion.runEvents?.runEvents?.length && - assertion.runEvents.runEvents[0].status === AssertionRunStatus.Complete && - assertion.runEvents.runEvents[0], - lastEvaluationTimeMs: - assertion.runEvents?.runEvents?.length && assertion.runEvents.runEvents[0].timestampMillis, - lastEvaluationResult: - assertion.runEvents?.runEvents?.length && - assertion.runEvents.runEvents[0].status === AssertionRunStatus.Complete && - assertion.runEvents.runEvents[0].result?.type, - lastEvaluationUrl: - assertion.runEvents?.runEvents?.length && - assertion.runEvents.runEvents[0].status === AssertionRunStatus.Complete && - assertion.runEvents.runEvents[0].result?.externalUrl, - assertion, - monitor: - (assertion as any).monitor?.relationships?.length && (assertion as any).monitor?.relationships[0].entity, - })); - - const assertionsTableCols = [ - { - title: '', - dataIndex: '', - key: '', - render: (_, record: any) => { - const selected = selectedUrns?.some((selectedUrn) => selectedUrn === record.urn); - return ( - - {showSelect && ( - e.stopPropagation()} - onChange={() => onSelect?.(record.urn as string)} - /> - )} - {/* setFocusAssertionUrn(record.urn)} - /> */} - - ); - }, - }, - { - title: '', - dataIndex: '', - key: '', - render: (_, record: any) => { - // TODO: Add permission for editing contract. - const isSqlAssertion = record.type === AssertionType.Sql; - return ( - <> - {/* {(showMenu && ( - - )) || - undefined} */} - - ); - }, - }, - ]; - - return ( - <> - { - return ( - (record.urn === focusAssertionUrn && 'acryl-selected-assertions-table-row') || - 'acryl-assertions-table-row' - ); - }} - columns={assertionsTableCols} - dataSource={assertionsTableData} - rowKey="urn" - locale={{ - emptyText: , - }} - onRow={(record) => { - return { - onClick: (_) => { - if (showSelect) { - onSelect?.(record.urn as string); - } else { - setFocusAssertionUrn(record.urn); - } - }, - }; - }} - showHeader={false} - pagination={false} - /> - {/* {focusAssertionUrn && ( - setFocusAssertionUrn(null)} - refetch={refetch} - /> - )} */} - - ); -}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractAssertionGroupSelect.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractAssertionGroupSelect.tsx index 18c7200a12a4c0..c040eb13ae12ee 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractAssertionGroupSelect.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractAssertionGroupSelect.tsx @@ -2,8 +2,8 @@ import React from 'react'; import styled from 'styled-components'; import { Assertion } from '../../../../../../../../types.generated'; import { ANTD_GRAY } from '../../../../../constants'; -import { AcrylDatasetAssertionsList } from '../../AcrylAssertionsList'; import { DataContractCategoryType } from './types'; +import { DatasetAssertionsList } from '../../DatasetAssertionsList'; const Category = styled.div` padding: 20px; @@ -42,15 +42,15 @@ export const DataContractAssertionGroupSelect = ({ {category} {!multiple && `(Choose 1)`} - ); diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposal.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposal.tsx index 31985fc111900b..193a13eb19cb85 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposal.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposal.tsx @@ -120,155 +120,29 @@ const StyledEyeOutlined = styled(EyeOutlined)` * Displaying a Data Contract proposal for an entity. */ -export const DataContractProposal = ({ showContractBuilder, refetch, entityUrn, entityType }: any) => { - const { urn } = useEntityData(); - // const [acceptProposalMutation] = useAcceptProposalMutation(); - // const [rejectProposalMutation] = useRejectProposalMutation(); - // const { data, refetch: contractRefetch } = useGetContractProposalsQuery({ - // variables: { - // urn, - // }, - // }); - const data: any = { - listActionRequests: { - total: 0, - actionRequests: [], - __typename: 'ListActionRequestsResult', - }, - }; - const contractRefetch = () => null; - - useEffect(() => { - contractRefetch(); - }, [contractRefetch]); - - const acceptProposal = (actionRequestUrn) => { - Modal.confirm({ - title: 'Accept Proposed Contract', - content: 'Are you sure you want to accept this proposal? New assertions will be created for this dataset.', - okText: 'Yes', - onOk() { - // acceptProposalMutation({ variables: { urn: actionRequestUrn } }) - // .then(() => { - // analytics.event({ - // type: EventType.EntityActionEvent, - // actionType: 'ProposalAccepted', //EntityActionType.ProposalAccepted, - // actionQualifier: 'DATA_CONTRACT', //ActionRequestType.DataContract, - // entityType, - // entityUrn, - // }); - // setTimeout(() => refetch(), 3000); - // message.success('Successfully accepted the proposal!'); - // }) - // .catch((err) => { - // console.log(err); - // message.error('Failed to accept proposal. :('); - // }); - }, - }); - }; - - const rejectProposal = (actionRequestUrn) => { - Modal.confirm({ - title: 'Reject Proposed Contract', - content: - 'Are you sure you want to reject this proposal? Proposals will no longer be created for this dataset.', - okText: 'Yes', - onOk() { - // rejectProposalMutation({ variables: { urn: actionRequestUrn } }) - // .then(() => { - // analytics.event({ - // type: EventType.EntityActionEvent, - // actionType: 'ProposalRejected', //EntityActionType.ProposalRejected, - // actionQualifier: 'DATA_CONTRACT', // ActionRequestType.DataContract, - // entityType, - // entityUrn, - // }); - // contractRefetch(); - // setTimeout(() => refetch(), 3000); - // message.success('Rejected the proposal.'); - // }) - // .catch((err) => { - // console.log(err); - // message.error('Failed to reject proposal. :('); - // }); - }, - }); - }; - - // Extract the Contract which is being proposed. Note that this only goes to the current user if they are able to approve. - const hasContractProposal = - ((data?.listActionRequests?.total || 0) > 0 && data?.listActionRequests?.actionRequests?.length) || undefined; - const contractActionRequest = hasContractProposal && data?.listActionRequests?.actionRequests[0]; - const actionRequestStatus = contractActionRequest && contractActionRequest.status; - const actionRequestParams = contractActionRequest && contractActionRequest.params; - const actionRequestUrn = contractActionRequest && contractActionRequest.urn; - const contractProposal = actionRequestParams && actionRequestParams?.dataContractProposal; - const isActiveProposal = contractProposal && actionRequestStatus === 'PENDING'; //ActionRequestStatus.Pending; - +export const DataContractProposal = ({ showContractBuilder }: any) => { return ( - {(isActiveProposal && ) || ( - - )} - {(isActiveProposal && ( - <> - - There is a contract proposal pending review - - - - )) || ( - - No contract found -
- - A contract does not yet exist for this dataset - -
-
- )} + + No contract found +
+ + A contract does not yet exist for this dataset + +
+
- {(isActiveProposal && ( - - acceptProposal(actionRequestUrn as any)}> - - ACCEPT - - rejectProposal(actionRequestUrn as any)}> - - REJECT - - - - CREATE - - - )) || ( - - - - CREATE - - - )} + + + + CREATE + +
- {isActiveProposal && contractProposal && contractProposal?.freshness && ( - - )} - {isActiveProposal && contractProposal && contractProposal?.schema && ( - - )} - {isActiveProposal && contractProposal && contractProposal?.dataQuality && ( - - )}
); }; diff --git a/datahub-web-react/src/graphql/actionRequest.graphql b/datahub-web-react/src/graphql/actionRequest.graphql deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/datahub-web-react/src/graphql/contract.graphql b/datahub-web-react/src/graphql/contract.graphql index 2a17cbed1ee942..225a63eb599fe6 100644 --- a/datahub-web-react/src/graphql/contract.graphql +++ b/datahub-web-react/src/graphql/contract.graphql @@ -19,17 +19,17 @@ fragment dataContractDetails on DataContract { properties { freshness { assertion { - ...assertionDetailsWithMonitors + ...assertionDetails } } schema { assertion { - ...assertionDetailsWithMonitors + ...assertionDetails } } dataQuality { assertion { - ...assertionDetailsWithMonitors + ...assertionDetails } } } @@ -51,4 +51,3 @@ mutation upsertDataContract($input: UpsertDataContractInput!) { ...dataContractDetails } } - From 3882ecef377df0a1890e6744219ffa2e166313a2 Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Tue, 4 Jun 2024 17:33:59 +0530 Subject: [PATCH 09/26] feat(ui/data-contract): remove other acryl files and create OSS files --- ...Proposal.tsx => DataContractEmptyState.tsx} | 18 +++++++++--------- .../contract/DataContractSummary.tsx | 2 +- .../contract/DataContractSummaryFooter.tsx | 2 +- .../Validations/contract/DataContractTab.tsx | 6 +++--- .../contract/builder/DataContractBuilder.tsx | 2 +- .../Dataset/Validations/contract/utils.tsx | 4 ++-- .../Validations/shared/styledComponents.tsx | 2 +- .../Validations/{acrylTypes.tsx => types.tsx} | 0 .../Validations/{acrylUtils.tsx => utils.tsx} | 2 +- 9 files changed, 19 insertions(+), 19 deletions(-) rename datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/{proposal/DataContractProposal.tsx => DataContractEmptyState.tsx} (86%) rename datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/{acrylTypes.tsx => types.tsx} (100%) rename datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/{acrylUtils.tsx => utils.tsx} (99%) diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposal.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState.tsx similarity index 86% rename from datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposal.tsx rename to datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState.tsx index 193a13eb19cb85..c52ee97e905fc1 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposal.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState.tsx @@ -9,7 +9,7 @@ import { PlusOutlined, StopOutlined, } from '@ant-design/icons'; -import { useEntityData } from '../../../../../EntityContext'; +import { useEntityData } from '../../../../EntityContext'; // import { useGetContractProposalsQuery } from '../../../../../../../../graphql/contract.generated'; // import { // ActionRequestStatus, @@ -17,17 +17,17 @@ import { useEntityData } from '../../../../../EntityContext'; // DataContractProposalParams, // EntityType, // } from '../../../../../../../../types.generated'; -import { DataContractProposalDescription } from './DataContractProposalDescription'; +import { DataContractProposalDescription } from './proposal/DataContractProposalDescription'; // import { // useAcceptProposalMutation, // useRejectProposalMutation, // } from '../../../../../../../../graphql/actionRequest.generated'; -import { ANTD_GRAY } from '../../../../../constants'; -import { FAILURE_COLOR_HEX } from '../../../../Incident/incidentUtils'; -import { FreshnessContractSummary } from '../FreshnessContractSummary'; -import { SchemaContractSummary } from '../SchemaContractSummary'; -import { DataQualityContractSummary } from '../DataQualityContractSummary'; -import analytics, { EntityActionType, EventType } from '../../../../../../../analytics'; +import { ANTD_GRAY } from '../../../../constants'; +import { FAILURE_COLOR_HEX } from '../../../Incident/incidentUtils'; +import { FreshnessContractSummary } from './FreshnessContractSummary'; +import { SchemaContractSummary } from './SchemaContractSummary'; +import { DataQualityContractSummary } from './DataQualityContractSummary'; +import analytics, { EntityActionType, EventType } from '../../../../../../analytics'; const Container = styled.div``; @@ -120,7 +120,7 @@ const StyledEyeOutlined = styled(EyeOutlined)` * Displaying a Data Contract proposal for an entity. */ -export const DataContractProposal = ({ showContractBuilder }: any) => { +export const DataContractEmptyState = ({ showContractBuilder }: any) => { return ( diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummary.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummary.tsx index c51c2b9e9dca08..c280c4d6eecd0b 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummary.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummary.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import EditIcon from '@mui/icons-material/Edit'; import { Button, Typography } from 'antd'; import { DataContractState } from '../../../../../../../types.generated'; -import { AssertionStatusSummary } from '../acrylTypes'; +import { AssertionStatusSummary } from '../types'; import { getContractSummaryIcon, getContractSummaryTitle, getContractSummaryMessage } from './utils'; import { ANTD_GRAY } from '../../../../constants'; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummaryFooter.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummaryFooter.tsx index 29e41553149f7b..6a892ebe2417a7 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummaryFooter.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummaryFooter.tsx @@ -4,7 +4,7 @@ import { ArrowRightOutlined } from '@ant-design/icons'; import { Button } from 'antd'; import { Assertion } from '../../../../../../../types.generated'; import { StyledCheckOutlined, StyledCloseOutlined, StyledExclamationOutlined } from '../shared/styledComponents'; -import { getAssertionsSummary } from '../acrylUtils'; +import { getAssertionsSummary } from '../utils'; import { ANTD_GRAY, REDESIGN_COLORS } from '../../../../constants'; const Container = styled.div` diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx index 0a50213bea550c..6e4b6a7b0bccd2 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx @@ -3,14 +3,14 @@ import styled from 'styled-components'; import { useGetDatasetContractQuery } from '../../../../../../../graphql/contract.generated'; import { DataContractState } from '../../../../../../../types.generated'; import { useEntityData } from '../../../../EntityContext'; -import { DataContractProposal } from './proposal/DataContractProposal'; +import { DataContractEmptyState } from './DataContractEmptyState'; import { DataContractSummary } from './DataContractSummary'; import { DataQualityContractSummary } from './DataQualityContractSummary'; import { SchemaContractSummary } from './SchemaContractSummary'; import { FreshnessContractSummary } from './FreshnessContractSummary'; import { DataContractBuilderModal } from './builder/DataContractBuilderModal'; import { createBuilderState } from './builder/utils'; -import { getAssertionsSummary } from '../acrylUtils'; +import { getAssertionsSummary } from '../utils'; const Container = styled.div` display: flex; @@ -104,7 +104,7 @@ export const DataContractTab = () => { )) || ( - setShowContractBuilder(true)} entityUrn={urn} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx index 839f67c337518d..c8ae1ff7e4a1d9 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx @@ -20,7 +20,7 @@ import { AssertionWithMonitorDetails, createAssertionGroups, tryExtractMonitorDetailsFromAssertionsWithMonitorsQuery, -} from '../../acrylUtils'; +} from '../../utils'; import { DataContractAssertionGroupSelect } from './DataContractAssertionGroupSelect'; import { ANTD_GRAY } from '../../../../../constants'; import { DATA_QUALITY_ASSERTION_TYPES } from '../utils'; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/utils.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/utils.tsx index c541ceec6f8db0..c47b480c0451c6 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/utils.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/utils.tsx @@ -8,9 +8,9 @@ import { } from '@ant-design/icons'; import { Assertion, AssertionType, DataContract, DataContractState } from '../../../../../../../types.generated'; import { ANTD_GRAY } from '../../../../constants'; -import { FAILURE_COLOR_HEX, SUCCESS_COLOR_HEX, WARNING_COLOR_HEX } from '../acrylUtils'; +import { FAILURE_COLOR_HEX, SUCCESS_COLOR_HEX, WARNING_COLOR_HEX } from '../utils'; import { DataContractCategoryType } from './builder/types'; -import { AssertionStatusSummary } from '../acrylTypes'; +import { AssertionStatusSummary } from '../types'; export const getContractSummaryIcon = (state: DataContractState, summary: AssertionStatusSummary) => { if (state === DataContractState.Pending) { diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/shared/styledComponents.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/shared/styledComponents.tsx index a43e8ad841eeeb..14651899a355fa 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/shared/styledComponents.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/shared/styledComponents.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components'; import { CheckOutlined, ClockCircleOutlined, CloseOutlined, ExclamationCircleOutlined } from '@ant-design/icons'; import { ANTD_GRAY } from '../../../../constants'; -import { FAILURE_COLOR_HEX, SUCCESS_COLOR_HEX, WARNING_COLOR_HEX } from '../acrylUtils'; +import { FAILURE_COLOR_HEX, SUCCESS_COLOR_HEX, WARNING_COLOR_HEX } from '../utils'; export const StyledCheckOutlined = styled(CheckOutlined)` color: ${SUCCESS_COLOR_HEX}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylTypes.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/types.tsx similarity index 100% rename from datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylTypes.tsx rename to datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/types.tsx diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylUtils.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx similarity index 99% rename from datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylUtils.tsx rename to datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx index 4dbf53a05b20a9..0b645cc0574778 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/acrylUtils.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx @@ -26,7 +26,7 @@ import { // MonitorMode, } from '../../../../../../types.generated'; import { sortAssertions } from './assertionUtils'; -import { AssertionGroup, AssertionStatusSummary } from './acrylTypes'; +import { AssertionGroup, AssertionStatusSummary } from './types'; import { lowerFirstLetter } from '../../../../../shared/textUtil'; // import { useIngestionSourceForEntityQuery } from '../../../../../../graphql/ingestion.generated'; // import { From 25536da741f00a3c582dbacb4c1289d283556eaa Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Wed, 5 Jun 2024 17:09:58 +0530 Subject: [PATCH 10/26] feat(ui/data-contract): add condtional rendering the assertion list item on data contract --- .../Validations/DatasetAssertionsList.tsx | 144 ++++++++++++------ .../Validations/contract/DataContractTab.tsx | 1 - .../DataContractAssertionGroupSelect.tsx | 8 +- .../contract/builder/DataContractBuilder.tsx | 115 ++++---------- .../Validations/contract/builder/types.ts | 7 + .../shared/tabs/Dataset/Validations/utils.tsx | 2 +- 6 files changed, 136 insertions(+), 141 deletions(-) diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx index 3eccfb8931fc0f..7871634ba9b249 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx @@ -1,11 +1,11 @@ -import { Button, Dropdown, Empty, Image, message, Modal, Tag, Tooltip, Typography } from 'antd'; +import { Button, Dropdown, Empty, Image, message, Modal, Tag, Tooltip, Typography, Checkbox } from 'antd'; import React from 'react'; import styled from 'styled-components'; import { DeleteOutlined, DownOutlined, MoreOutlined, RightOutlined, StopOutlined } from '@ant-design/icons'; import { DatasetAssertionDescription } from './DatasetAssertionDescription'; import { StyledTable } from '../../../components/styled/StyledTable'; import { DatasetAssertionDetails } from './DatasetAssertionDetails'; -import { Assertion, AssertionRunStatus } from '../../../../../../types.generated'; +import { Assertion, AssertionRunStatus, DataContract } from '../../../../../../types.generated'; import { getResultColor, getResultIcon, getResultText } from './assertionUtils'; import { useDeleteAssertionMutation } from '../../../../../../graphql/assertion.generated'; import { capitalizeFirstLetterOnly } from '../../../../../shared/textUtil'; @@ -35,9 +35,20 @@ const StyledMoreOutlined = styled(MoreOutlined)` font-size: 18px; `; +const AssertionSelectCheckbox = styled(Checkbox)` + margin-right: 12px; +`; + type Props = { assertions: Array; onDelete?: (urn: string) => void; + contract?: DataContract; + // required for enabling menu/actions + showMenu?: boolean; + onSelect?: (assertionUrn: string) => void; + // required for enabling selection logic + showSelect?: boolean; + selectedUrns?: string[]; }; /** @@ -46,8 +57,16 @@ type Props = { * * Currently this component supports rendering Dataset Assertions only. */ -export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => { +export const DatasetAssertionsList = ({ + assertions, + onDelete, + showMenu = true, + showSelect, + onSelect, + selectedUrns, +}: Props) => { const [deleteAssertionMutation] = useDeleteAssertionMutation(); + console.log('selectedUrns>>>>', selectedUrns); const deleteAssertion = async (urn: string) => { try { @@ -102,9 +121,17 @@ export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => { const resultColor = (record.lastExecResult && getResultColor(record.lastExecResult)) || 'default'; const resultText = (record.lastExecResult && getResultText(record.lastExecResult)) || 'No Evaluations'; const resultIcon = (record.lastExecResult && getResultIcon(record.lastExecResult)) || ; + const selected = selectedUrns?.some((selectedUrn) => selectedUrn === record.urn); const { description } = record; return ( + {showSelect ? ( + e.stopPropagation()} + onChange={() => onSelect?.(record.urn as string)} + /> + ) : undefined}
@@ -125,37 +152,44 @@ export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => { title: '', dataIndex: '', key: '', - render: (_, record: any) => ( - - - - {(record.platform.properties?.logoUrl && ( - - )) || ( - - {record.platform.properties?.displayName || - capitalizeFirstLetterOnly(record.platform.name)} - - )} - - - - } trigger={['click']}> - - - - ), + render: (_, record: any) => { + return ( + <> + {showMenu && ( + + + + {(record.platform.properties?.logoUrl && ( + + )) || ( + + {record.platform.properties?.displayName || + capitalizeFirstLetterOnly(record.platform.name)} + + )} + + + + } trigger={['click']}> + + + + )} + + ); + }, }, ]; @@ -168,18 +202,36 @@ export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => { locale={{ emptyText: , }} - expandable={{ - defaultExpandAllRows: false, - expandRowByClick: true, - expandedRowRender: (record) => { - return ; - }, - expandIcon: ({ expanded, onExpand, record }: any) => - expanded ? ( - onExpand(record, e)} /> - ) : ( - onExpand(record, e)} /> - ), + expandable={ + showSelect + ? {} + : { + defaultExpandAllRows: false, + expandRowByClick: true, + expandedRowRender: (record) => { + return ( + + ); + }, + expandIcon: ({ expanded, onExpand, record }: any) => + expanded ? ( + onExpand(record, e)} /> + ) : ( + onExpand(record, e)} /> + ), + } + } + onRow={(record) => { + return { + onClick: (_) => { + if (showSelect) { + onSelect?.(record.urn as string); + } + }, + }; }} showHeader={false} pagination={false} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx index 6e4b6a7b0bccd2..5ca68b1ce96bcf 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx @@ -36,7 +36,6 @@ export const DataContractTab = () => { }, }); const [showContractBuilder, setShowContractBuilder] = useState(false); - console.log('data>>>>', data); const contract = data?.dataset?.contract; const schemaContracts = data?.dataset?.contract?.properties?.schema || []; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractAssertionGroupSelect.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractAssertionGroupSelect.tsx index c040eb13ae12ee..4dcfdf1fffceca 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractAssertionGroupSelect.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractAssertionGroupSelect.tsx @@ -44,10 +44,10 @@ export const DataContractAssertionGroupSelect = ({ ); const freshnessAssertions = assertionGroups.find((group) => group.type === AssertionType.Freshness)?.assertions || []; const schemaAssertions = assertionGroups.find((group) => group.type === AssertionType.DataSchema)?.assertions || []; + const dataSetAssertions = assertionGroups.find((group) => group.type === AssertionType.Dataset)?.assertions || []; const dataQualityAssertions = assertionGroups .filter((group) => DATA_QUALITY_ASSERTION_TYPES.has(group.type)) .flatMap((group) => group.assertions || []); @@ -112,80 +109,17 @@ export const DataContractBuilder = ({ entityUrn, entityType, initialState, onSub }); }; - /** - * Proposes the upsert to the Data Contract for an entity - */ - const proposeUpsertDataContract = () => { - // return proposeDataContractMutation({ - // variables: buildProposeDataContractMutationVariables( - // DataContractProposalOperationType.Overwrite, - // entityUrn, - // builderState, - // ), - // }) - // .then(({ errors }) => { - // if (!errors) { - // analytics.event({ - // type: EventType.EntityActionEvent, - // actionType: EntityActionType.ProposalCreated, - // actionQualifier: ActionRequestType.DataContract, - // entityType, - // entityUrn, - // }); - // message.success({ - // content: `Proposed Data Contract!`, - // duration: 3, - // }); - // onPropose?.(); - // } - // }) - // .catch(() => { - // message.destroy(); - // message.error({ content: 'Failed to propose Data Contract! An unexpected error occurred' }); - // }); - }; - - const onSelectFreshnessAssertion = (assertionUrn: string) => { - const selected = builderState.freshness?.assertionUrn === assertionUrn; - if (selected) { - setBuilderState({ - ...builderState, - freshness: undefined, - }); - } else { - setBuilderState({ - ...builderState, - freshness: { assertionUrn }, - }); - } - }; - - const onSelectSchemaAssertion = (assertionUrn: string) => { - const selected = builderState.schema?.assertionUrn === assertionUrn; + const onSelectDataAssertion = (assertionUrn: string, type: string) => { + const selected = builderState[type]?.some((c) => c.assertionUrn === assertionUrn); if (selected) { setBuilderState({ ...builderState, - schema: undefined, + [type]: builderState[type]?.filter((c) => c.assertionUrn !== assertionUrn), }); } else { setBuilderState({ ...builderState, - schema: { assertionUrn }, - }); - } - }; - - const onSelectDataQualityAssertion = (assertionUrn: string) => { - const selected = builderState.dataQuality?.some((c) => c.assertionUrn === assertionUrn); - if (selected) { - setBuilderState({ - ...builderState, - dataQuality: builderState.dataQuality?.filter((c) => c.assertionUrn !== assertionUrn), - }); - } else { - setBuilderState({ - ...builderState, - dataQuality: [...(builderState.dataQuality || []), { assertionUrn }], + [type]: [...(builderState[type] || []), { assertionUrn }], }); } }; @@ -209,7 +143,7 @@ export const DataContractBuilder = ({ entityUrn, entityType, initialState, onSub selectedUrns={ (builderState.freshness?.assertionUrn && [builderState.freshness?.assertionUrn]) || [] } - onSelect={onSelectFreshnessAssertion} + onSelect={(selectedUrn: string) => onSelectDataAssertion(selectedUrn, 'freshness')} /> )) || undefined} @@ -219,7 +153,7 @@ export const DataContractBuilder = ({ entityUrn, entityType, initialState, onSub assertions={schemaAssertions} multiple={false} selectedUrns={(builderState.schema?.assertionUrn && [builderState.schema?.assertionUrn]) || []} - onSelect={onSelectSchemaAssertion} + onSelect={(selectedUrn: string) => onSelectDataAssertion(selectedUrn, 'schema')} /> )) || undefined} @@ -228,19 +162,22 @@ export const DataContractBuilder = ({ entityUrn, entityType, initialState, onSub category={DataContractCategoryType.DATA_QUALITY} assertions={dataQualityAssertions} selectedUrns={builderState.dataQuality?.map((c) => c.assertionUrn) || []} - onSelect={onSelectDataQualityAssertion} + onSelect={(selectedUrn: string) => onSelectDataAssertion(selectedUrn, 'dataQuality')} /> )) || undefined} + {dataSetAssertions.length && ( + c.assertionUrn) || []} + onSelect={(selectedUrn: string) => onSelectDataAssertion(selectedUrn, 'dataset')} + /> + )} Cancel
- - - Propose - - Save diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/types.ts b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/types.ts index d527837efd72ef..40b3180e446aea 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/types.ts +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/types.ts @@ -22,16 +22,23 @@ export type DataContractBuilderState = { dataQuality?: { assertionUrn: string; }[]; + + // TODO check if this type is needed or not if not needed then remove it + dataset?: { + assertionUrn: string; + }[]; }; export const DEFAULT_BUILDER_STATE = { dataQuality: undefined, schema: undefined, freshness: undefined, + dataset: undefined, }; export enum DataContractCategoryType { FRESHNESS = 'Freshness', SCHEMA = 'Schema', DATA_QUALITY = 'Data Quality', + DATA_SET = 'Data Set', } diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx index 0b645cc0574778..84f766b225c285 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx @@ -273,7 +273,7 @@ export const getLegacyAssertionsSummary = (assertions: Assertion[]) => { // * // * @param assertions The assertions to group // */ -export const createAssertionGroups = (assertions: Array): AssertionGroup[] => { +export const createAssertionGroups = (assertions: Array): AssertionGroup[] => { // Pre-sort the list of assertions based on which has been most recently executed. assertions.sort(sortAssertions); From 0cb37e266edcba4ec35e9ddef02237400fbcf466 Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Thu, 6 Jun 2024 17:46:44 +0530 Subject: [PATCH 11/26] feat(ui/data-contract): removed unwanted code for data contract --- .../Validations/DatasetAssertionsList.tsx | 1 - .../contract/DataContractEmptyState.tsx | 72 +-------- .../Validations/contract/DataContractTab.tsx | 13 +- .../contract/builder/DataContractBuilder.tsx | 11 +- .../Validations/contract/builder/types.ts | 6 - .../Validations/contract/builder/utils.ts | 21 +-- .../Dataset/Validations/contract/utils.tsx | 7 +- .../shared/tabs/Dataset/Validations/utils.tsx | 138 +----------------- 8 files changed, 26 insertions(+), 243 deletions(-) diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx index 7871634ba9b249..34179a21a5c669 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx @@ -66,7 +66,6 @@ export const DatasetAssertionsList = ({ selectedUrns, }: Props) => { const [deleteAssertionMutation] = useDeleteAssertionMutation(); - console.log('selectedUrns>>>>', selectedUrns); const deleteAssertion = async (urn: string) => { try { diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState.tsx index c52ee97e905fc1..1ddeaf22eae331 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState.tsx @@ -1,33 +1,9 @@ import React, { useEffect } from 'react'; import styled from 'styled-components'; -import { Button, message, Modal, Typography } from 'antd'; -import { - CheckOutlined, - CloseOutlined, - EyeOutlined, - InfoCircleOutlined, - PlusOutlined, - StopOutlined, -} from '@ant-design/icons'; -import { useEntityData } from '../../../../EntityContext'; -// import { useGetContractProposalsQuery } from '../../../../../../../../graphql/contract.generated'; -// import { -// ActionRequestStatus, -// ActionRequestType, -// DataContractProposalParams, -// EntityType, -// } from '../../../../../../../../types.generated'; -import { DataContractProposalDescription } from './proposal/DataContractProposalDescription'; -// import { -// useAcceptProposalMutation, -// useRejectProposalMutation, -// } from '../../../../../../../../graphql/actionRequest.generated'; +import { Button, Typography } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; + import { ANTD_GRAY } from '../../../../constants'; -import { FAILURE_COLOR_HEX } from '../../../Incident/incidentUtils'; -import { FreshnessContractSummary } from './FreshnessContractSummary'; -import { SchemaContractSummary } from './SchemaContractSummary'; -import { DataQualityContractSummary } from './DataQualityContractSummary'; -import analytics, { EntityActionType, EventType } from '../../../../../../analytics'; const Container = styled.div``; @@ -66,14 +42,6 @@ const Actions = styled.div` margin-right: 20px; `; -const ApproveButton = styled(Button)` - margin-right: 12px; - background-color: ${(props) => props.theme.styles['primary-color']}; - border-color: ${(props) => props.theme.styles['primary-color']}; - color: white; - letter-spacing: 2px; -`; - const CreateButton = styled(Button)` margin-right: 12px; border-color: ${(props) => props.theme.styles['primary-color']}; @@ -86,41 +54,15 @@ const CreateButton = styled(Button)` } `; -const DenyButton = styled(Button)` - color: ${FAILURE_COLOR_HEX}; - border-color: ${FAILURE_COLOR_HEX}; - letter-spacing: 2px; - &&:hover { - color: white; - background-color: ${FAILURE_COLOR_HEX}; - border-color: ${FAILURE_COLOR_HEX}; - } - margin-right: 12px; -`; - -const StyledInfoCircleOutlined = styled(InfoCircleOutlined)` - margin-left: 4px; - font-size: 10px; - color: ${ANTD_GRAY[7]}; -`; - -const StyledEyeOutlined = styled(EyeOutlined)` - font-size: 24px; - color: ${ANTD_GRAY[7]}; -`; - -// type Props = { -// showContractBuilder: () => void; -// refetch: () => void; -// entityUrn: string; -// entityType?: EntityType; -// }; +type Props = { + showContractBuilder: () => void; +}; /** * Displaying a Data Contract proposal for an entity. */ -export const DataContractEmptyState = ({ showContractBuilder }: any) => { +export const DataContractEmptyState = ({ showContractBuilder }: Props) => { return ( diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx index 5ca68b1ce96bcf..87438d59cd3bad 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx @@ -91,7 +91,7 @@ export const DataContractTab = () => { undefined} )} - {/* + {(hasDataQualityContract && ( { /> )) || undefined} - */} + - )) || ( - setShowContractBuilder(true)} - entityUrn={urn} - entityType={entityType} - /> - )} + )) || setShowContractBuilder(true)} />} {showContractBuilder && ( group.type === AssertionType.Freshness)?.assertions || []; const schemaAssertions = assertionGroups.find((group) => group.type === AssertionType.DataSchema)?.assertions || []; - const dataSetAssertions = assertionGroups.find((group) => group.type === AssertionType.Dataset)?.assertions || []; const dataQualityAssertions = assertionGroups .filter((group) => DATA_QUALITY_ASSERTION_TYPES.has(group.type)) .flatMap((group) => group.assertions || []); @@ -166,14 +165,6 @@ export const DataContractBuilder = ({ entityUrn, entityType, initialState, onSub /> )) || undefined} - {dataSetAssertions.length && ( - c.assertionUrn) || []} - onSelect={(selectedUrn: string) => onSelectDataAssertion(selectedUrn, 'dataset')} - /> - )} Cancel diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/types.ts b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/types.ts index 40b3180e446aea..497bd03cdf8749 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/types.ts +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/types.ts @@ -23,22 +23,16 @@ export type DataContractBuilderState = { assertionUrn: string; }[]; - // TODO check if this type is needed or not if not needed then remove it - dataset?: { - assertionUrn: string; - }[]; }; export const DEFAULT_BUILDER_STATE = { dataQuality: undefined, schema: undefined, freshness: undefined, - dataset: undefined, }; export enum DataContractCategoryType { FRESHNESS = 'Freshness', SCHEMA = 'Schema', DATA_QUALITY = 'Data Quality', - DATA_SET = 'Data Set', } diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/utils.ts b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/utils.ts index 281fa4b3855a91..da2ae66d1ec9c9 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/utils.ts +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/utils.ts @@ -1,4 +1,4 @@ -import { DataContractProposalOperationType, DataContract } from '../../../../../../../../types.generated'; +import { DataContract } from '../../../../../../../../types.generated'; import { DataContractBuilderState, DataContractCategoryType } from './types'; /** @@ -38,25 +38,6 @@ export const buildUpsertDataContractMutationVariables = (entityUrn: string, stat }; }; -/** - * Constructs the input variables required for proposing a data contract using graphql - */ -export const buildProposeDataContractMutationVariables = ( - operationType: DataContractProposalOperationType, - entityUrn: string, - state: DataContractBuilderState, -) => { - return { - input: { - operationType, - entityUrn, - freshness: (state.freshness && [state.freshness]) || [], - schema: (state.schema && [state.schema]) || [], - dataQuality: state.dataQuality || [], - }, - }; -}; - /** * Constructs the input variables required for removing an assertion from a data contract using graphql. */ diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/utils.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/utils.tsx index c47b480c0451c6..cc2e1bb7b386e7 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/utils.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/utils.tsx @@ -106,4 +106,9 @@ export const getDataContractCategoryFromAssertion = (assertion: Assertion) => { return DataContractCategoryType.DATA_QUALITY; }; -export const DATA_QUALITY_ASSERTION_TYPES = new Set([AssertionType.Volume, AssertionType.Sql, AssertionType.Field]); +export const DATA_QUALITY_ASSERTION_TYPES = new Set([ + AssertionType.Volume, + AssertionType.Sql, + AssertionType.Field, + AssertionType.Dataset, +]); diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx index 84f766b225c285..7192d9c03da7f3 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx @@ -14,25 +14,10 @@ import { CodeOutlined, ExclamationCircleOutlined, } from '@ant-design/icons'; -import { - Assertion, - AssertionResultType, - AssertionType, - // CronSchedule, - // DatasetFreshnessSourceType, - // DatasetVolumeSourceType, - EntityType, - // Monitor, - // MonitorMode, -} from '../../../../../../types.generated'; +import { Assertion, AssertionResultType, AssertionType, EntityType } from '../../../../../../types.generated'; import { sortAssertions } from './assertionUtils'; import { AssertionGroup, AssertionStatusSummary } from './types'; import { lowerFirstLetter } from '../../../../../shared/textUtil'; -// import { useIngestionSourceForEntityQuery } from '../../../../../../graphql/ingestion.generated'; -// import { -// GetDatasetAssertionsWithMonitorsQuery, -// MonitorDetailsFragment, -// } from '../../../../../../graphql/monitor.generated'; export const SUCCESS_COLOR_HEX = '#52C41A'; export const FAILURE_COLOR_HEX = '#F5222D'; @@ -191,28 +176,12 @@ const getAssertionGroupTypeIcon = (type: AssertionType) => { return ASSERTION_TYPE_TO_INFO.has(type) ? ASSERTION_TYPE_TO_INFO.get(type).icon : undefined; }; -export type AssertionWithMonitorDetails = Assertion & { - monitors?: any[]; // should almost always have 0-1 items -}; - -export const tryExtractMonitorDetailsFromAssertionsWithMonitorsQuery = ( - queryData?: any, //GetDatasetAssertionsWithMonitorsQuery, -): AssertionWithMonitorDetails[] | undefined => { - return queryData?.dataset?.assertions?.assertions?.map((assertion) => ({ - ...(assertion as Assertion), - monitors: - assertion.monitor?.relationships?.filter((r) => r.entity?.__typename === 'Monitor').map((r) => r.entity) ?? - [], - // .map((r) => r.entity as MonitorDetailsFragment) ?? [], - })); -}; - /** * Returns a status summary for the assertions associated with a Dataset. * * @param assertions The assertions to extract the summary for */ -export const getAssertionsSummary = (assertions: AssertionWithMonitorDetails[]): AssertionStatusSummary => { +export const getAssertionsSummary = (assertions: Assertion[]): AssertionStatusSummary => { const summary = { passing: 0, failing: 0, @@ -221,15 +190,6 @@ export const getAssertionsSummary = (assertions: AssertionWithMonitorDetails[]): totalAssertions: assertions.length, }; assertions.forEach((assertion) => { - // Skip inactive monitors - // NOTE: we don't assert that the status is Active, because in cases of external assertions they won't have monitors - const maybeInactiveMonitor = assertion.monitors?.find( - (item) => item.info?.status.mode === 'IN_ACTIVE', //MonitorMode.Inactive, - ); - if (maybeInactiveMonitor) { - return; - } - if ((assertion.runEvents?.runEvents?.length || 0) > 0) { const mostRecentRun = assertion.runEvents?.runEvents?.[0]; const resultType = mostRecentRun?.result?.type; @@ -239,13 +199,12 @@ export const getAssertionsSummary = (assertions: AssertionWithMonitorDetails[]): if (AssertionResultType.Failure === resultType) { summary.failing++; } - // if (AssertionResultType.Error === resultType) { - // if ('ERROR' === resultType) { - // summary.erroring++; - // } - // if ('INIT' !== resultType) { - // summary.total++; // only count assertions for which there is one completed run event, ignoring INIT statuses! - // } + if (AssertionResultType.Error === resultType) { + summary.erroring++; + } + if (AssertionResultType.Init !== resultType) { + summary.total++; // only count assertions for which there is one completed run event, ignoring INIT statuses! + } } }); return summary; @@ -334,23 +293,6 @@ export const getAssertionGroupSummaryMessage = (summary: AssertionStatusSummary) return 'Some assertions are failing'; }; -/** - * Returns the next scheduled run of a cron schedule, in the local timezone of teh user. - * - * @param schedule a cron schedule - */ -// export const getNextScheduleEvaluationTimeMs = (schedule: CronSchedule) => { -// try { -// const interval = cronParser.parseExpression(schedule.cron, { tz: schedule.timezone }); -// const nextDate = interval.next().toDate(); // Get next date as JavaScript Date object -// const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; -// const nextDateInUserTz = moment.tz(nextDate, userTimezone); // Convert to user's timezone -// return nextDateInUserTz.valueOf(); -// } catch (e) { -// return undefined; -// } -// }; - export const getAssertionTypesForEntityType = (entityType: EntityType, monitorsConnectionForEntityExists: boolean) => { return ASSERTION_INFO.filter((type) => type.entityTypes.includes(entityType)).map((type) => ({ ...type, @@ -358,41 +300,6 @@ export const getAssertionTypesForEntityType = (entityType: EntityType, monitorsC })); }; -export const isMonitorActive = (monitor: any) => { - return monitor.info?.status?.mode === 'active'; -}; - -export const getCronAsText = (interval: string, options: { verbose: boolean } = { verbose: false }) => { - const { verbose } = options; - if (interval) { - try { - return { - text: `${lowerFirstLetter(cronstrue.toString(interval, { verbose }))}.`, - error: false, - }; - } catch (e) { - return { - text: undefined, - error: true, - }; - } - } - return { - text: undefined, - error: false, - }; -}; - -export const canManageAssertionMonitor = (monitor: any, connectionForEntityExists: boolean) => { - if (connectionForEntityExists) return true; - - const assertionParameters = monitor?.info?.assertionMonitor?.assertions?.[0]?.parameters; - return ( - assertionParameters?.datasetFreshnessParameters?.sourceType === 'DATAHUB_OPERATION' || //DatasetFreshnessSourceType.DatahubOperation || - assertionParameters?.datasetVolumeParameters?.sourceType === 'DATAHUB_DATASET_PROFILE' //DatasetVolumeSourceType.DatahubDatasetProfile - ); -}; - // export const getEntityUrnForAssertion = (assertion: Assertion) => { export const getEntityUrnForAssertion = (assertion: any) => { if (assertion.info?.type === AssertionType.Dataset) { @@ -416,32 +323,3 @@ export const getEntityUrnForAssertion = (assertion: any) => { console.error(`Unable to extract entity urn from unrecognized assertion with type ${assertion.info?.type}`); return undefined; }; - -// export const useConnectionForEntityExists = (entityUrn: string) => { -// const { data: ingestionSourceData } = useIngestionSourceForEntityQuery({ -// variables: { urn: entityUrn as string }, -// fetchPolicy: 'cache-first', -// }); - -// return !!ingestionSourceData?.ingestionSourceForEntity?.urn; -// }; - -/** - * Checks if a connection exists for an entity that is able to run test assertion queries - * @param entityUrn - * @returns {boolean} optimistically returns true - */ -// export const useConnectionWithTestAssertionCapabilitiesForEntityExists = (entityUrn: string): boolean => { -// const { data: ingestionSourceData } = useIngestionSourceForEntityQuery({ -// variables: { urn: entityUrn as string }, -// fetchPolicy: 'cache-first', -// }); - -// // Only embedded executors can run tests right now -// // If executorId is null, we'll assume it is an embedded executor. -// // If the executorId starts with 'default', we assume it's an embedded executor -// // See setup docs: https://www.notion.so/acryldata/How-to-configure-Remote-Executor-e9ed044b438d4789afcd530952d73944?pvs=4#14237a6d6dd04fcfb2abd45f16c6d63c -// // and design docs: https://www.notion.so/acryldata/Remote-Executor-V2-Design-593d41280c4a4e34805def00b3f47a65?pvs=4#fe2a4481fbe74f379eb35cd10546b3b8 -// const maybeExecutorId = ingestionSourceData?.ingestionSourceForEntity?.config?.executorId; -// return !maybeExecutorId || maybeExecutorId.toLowerCase().startsWith('default'); -// }; From 2d872c8a775d30e31d08b00d15c5726b1566814f Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Thu, 6 Jun 2024 23:25:36 +0530 Subject: [PATCH 12/26] feat(ui/data-contract): add datacontract image for assertion list --- .../tabs/Dataset/Validations/Assertions.tsx | 7 +++ .../Validations/DatasetAssertionsList.tsx | 58 ++++++++++++++++++- .../Dataset/Validations/assertionUtils.tsx | 2 +- 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx index b3086d78670121..86d4b5234c632f 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx @@ -6,6 +6,7 @@ import { DatasetAssertionsList } from './DatasetAssertionsList'; import { DatasetAssertionsSummary } from './DatasetAssertionsSummary'; import { sortAssertions } from './assertionUtils'; import { combineEntityDataWithSiblings, useIsSeparateSiblingsMode } from '../../../siblingUtils'; +import { useGetDatasetContractQuery } from '../../../../../../graphql/contract.generated'; /** * Returns a status summary for the assertions associated with a Dataset. @@ -46,6 +47,11 @@ export const Assertions = () => { const combinedData = isHideSiblingMode ? data : combineEntityDataWithSiblings(data); const [removedUrns, setRemovedUrns] = useState([]); + const { data: contractData, refetch: contractRefetch } = useGetDatasetContractQuery({ + variables: { urn }, + fetchPolicy: 'cache-first', + }); + const contract = contractData?.dataset?.contract as any; const assertions = (combinedData && combinedData.dataset?.assertions?.assertions?.map((assertion) => assertion as Assertion)) || []; @@ -67,6 +73,7 @@ export const Assertions = () => { setRemovedUrns([...removedUrns, assertionUrn]); setTimeout(() => refetch(), 3000); }} + contract={contract} /> )} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx index 34179a21a5c669..b6ed745d14806e 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx @@ -1,15 +1,26 @@ import { Button, Dropdown, Empty, Image, message, Modal, Tag, Tooltip, Typography, Checkbox } from 'antd'; import React from 'react'; import styled from 'styled-components'; -import { DeleteOutlined, DownOutlined, MoreOutlined, RightOutlined, StopOutlined } from '@ant-design/icons'; +import { + DeleteOutlined, + DownOutlined, + MoreOutlined, + RightOutlined, + StopOutlined, + AuditOutlined, +} from '@ant-design/icons'; import { DatasetAssertionDescription } from './DatasetAssertionDescription'; import { StyledTable } from '../../../components/styled/StyledTable'; import { DatasetAssertionDetails } from './DatasetAssertionDetails'; -import { Assertion, AssertionRunStatus, DataContract } from '../../../../../../types.generated'; +import { Assertion, AssertionRunStatus, DataContract, EntityType } from '../../../../../../types.generated'; import { getResultColor, getResultIcon, getResultText } from './assertionUtils'; import { useDeleteAssertionMutation } from '../../../../../../graphql/assertion.generated'; import { capitalizeFirstLetterOnly } from '../../../../../shared/textUtil'; import AssertionMenu from './AssertionMenu'; +import { Link } from 'react-router-dom'; +import { REDESIGN_COLORS } from '../../../constants'; +import { useEntityRegistry } from '../../../../../useEntityRegistry'; +import { isAssertionPartOfContract } from './contract/utils'; const ResultContainer = styled.div` display: flex; @@ -39,6 +50,12 @@ const AssertionSelectCheckbox = styled(Checkbox)` margin-right: 12px; `; +const DataContractLogo = styled(AuditOutlined)` + margin-left: 8px; + font-size: 16px; + color: ${REDESIGN_COLORS.BLUE}; +`; + type Props = { assertions: Array; onDelete?: (urn: string) => void; @@ -64,8 +81,10 @@ export const DatasetAssertionsList = ({ showSelect, onSelect, selectedUrns, + contract, }: Props) => { const [deleteAssertionMutation] = useDeleteAssertionMutation(); + const entityRegistry = useEntityRegistry(); const deleteAssertion = async (urn: string) => { try { @@ -121,6 +140,11 @@ export const DatasetAssertionsList = ({ const resultText = (record.lastExecResult && getResultText(record.lastExecResult)) || 'No Evaluations'; const resultIcon = (record.lastExecResult && getResultIcon(record.lastExecResult)) || ; const selected = selectedUrns?.some((selectedUrn) => selectedUrn === record.urn); + const isPartOfContract = contract && isAssertionPartOfContract(record, contract); + const assertionEntityUrn = record.urn; + console.log('isPartOfContract>>>>>', isPartOfContract); + console.log('assertionEntityUrn>>>>>', assertionEntityUrn); + const { description } = record; return ( @@ -143,6 +167,36 @@ export const DatasetAssertionsList = ({ description={description} assertionInfo={record.datasetAssertionInfo} /> + {/* TODO work on below condition to handle assertionEntityUrn */} + {/* {(isPartOfContract && assertionEntityUrn && ( */} + {(isPartOfContract && ( + + Part of Data Contract{' '} + + view + + + } + > + + + + + )) || + undefined} ); }, diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/assertionUtils.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/assertionUtils.tsx index 1eaacb36515a1b..341742f407f73c 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/assertionUtils.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/assertionUtils.tsx @@ -18,7 +18,7 @@ export const sortAssertions = (a, b) => { if (!b.runEvents?.runEvents?.length) { return -1; } - return b.runEvents.runEvents[0].timestampMillis - a.runEvents.runEvents[0].timestampMillis; + return b.runEvents?.runEvents[0]?.timestampMillis - a.runEvents?.runEvents[0]?.timestampMillis; }; /** From 7915c5f640939852722196923efd87d2f0d3f2f0 Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Fri, 7 Jun 2024 19:21:47 +0530 Subject: [PATCH 13/26] feat(ui/data-contract): add contract icon assigned to assertion --- datahub-web-react/package.json | 3 +- datahub-web-react/src/app/analytics/event.ts | 1 - .../Validations/DatasetAssertionsList.tsx | 9 ++- .../DataContractProposalDescription.tsx | 47 ---------------- .../proposal/DataContractProposalModal.tsx | 54 ------------------ .../Validations/fieldDescriptionUtils.ts | 56 ++++++++++++++----- .../shared/tabs/Dataset/Validations/utils.tsx | 1 - datahub-web-react/src/graphql/monitor.graphql | 11 ---- datahub-web-react/yarn.lock | 12 ---- 9 files changed, 50 insertions(+), 144 deletions(-) delete mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposalDescription.tsx delete mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposalModal.tsx delete mode 100644 datahub-web-react/src/graphql/monitor.graphql diff --git a/datahub-web-react/package.json b/datahub-web-react/package.json index 69807801e7ae8f..553a7f178a390b 100644 --- a/datahub-web-react/package.json +++ b/datahub-web-react/package.json @@ -41,7 +41,6 @@ "analytics": "^0.8.9", "antd": "4.24.7", "color-hash": "^2.0.1", - "cron-parser": "^4.8.1", "cronstrue": "^1.122.0", "d3-scale": "^4.0.2", "dayjs": "^1.11.7", @@ -152,4 +151,4 @@ "prosemirror-transform": "1.2.2", "prosemirror-view": "1.13.4" } -} +} \ No newline at end of file diff --git a/datahub-web-react/src/app/analytics/event.ts b/datahub-web-react/src/app/analytics/event.ts index 2d3a3fa781a22f..d63b731c720426 100644 --- a/datahub-web-react/src/app/analytics/event.ts +++ b/datahub-web-react/src/app/analytics/event.ts @@ -312,7 +312,6 @@ export interface EntityActionEvent extends BaseEvent { actionType: string; entityType?: EntityType; entityUrn: string; - actionQualifier?: string; } export interface BatchEntityActionEvent extends BaseEvent { diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx index b6ed745d14806e..3449e47b655ada 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx @@ -17,10 +17,12 @@ import { getResultColor, getResultIcon, getResultText } from './assertionUtils'; import { useDeleteAssertionMutation } from '../../../../../../graphql/assertion.generated'; import { capitalizeFirstLetterOnly } from '../../../../../shared/textUtil'; import AssertionMenu from './AssertionMenu'; -import { Link } from 'react-router-dom'; +import { Link, useParams } from 'react-router-dom'; import { REDESIGN_COLORS } from '../../../constants'; import { useEntityRegistry } from '../../../../../useEntityRegistry'; import { isAssertionPartOfContract } from './contract/utils'; +import { decodeUrn } from '../../../utils'; +import { useEntityData } from '../../../EntityContext'; const ResultContainer = styled.div` display: flex; @@ -83,6 +85,7 @@ export const DatasetAssertionsList = ({ selectedUrns, contract, }: Props) => { + const entityData = useEntityData(); const [deleteAssertionMutation] = useDeleteAssertionMutation(); const entityRegistry = useEntityRegistry(); @@ -177,7 +180,7 @@ export const DatasetAssertionsList = ({ @@ -189,7 +192,7 @@ export const DatasetAssertionsList = ({ diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposalDescription.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposalDescription.tsx deleted file mode 100644 index 52dea63959cba3..00000000000000 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposalDescription.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { Typography } from 'antd'; -import { DataContractProposalParams } from '../../../../../../../../types.generated'; -import { DBT_URN } from '../../../../../../../ingest/source/builder/constants'; - -/** - * Returns true if the contract is implemented externally, - * e.g. within a source system like Dbt. - * - * This differs on a per-system basis. - */ -const isExternalContract = (urn) => { - if (urn.includes(DBT_URN)) { - return { isExternal: true, platformName: 'dbt' }; - } - return { isExternal: false, platformName: undefined }; -}; - -/** - * Returns the number of assertions that are involved in a particular Data Contract Proposal. - */ -const getAssertionCount = (proposal: DataContractProposalParams) => { - return (proposal.freshness?.length || 0) + (proposal.dataQuality?.length || 0) + (proposal.schema?.length || 0); -}; - -type Props = { - urn: string; - proposal: DataContractProposalParams; -}; - -/** - * A description for a Data Contract Proposal - */ -export const DataContractProposalDescription = ({ urn, proposal }: Props) => { - const { isExternal, platformName } = isExternalContract(urn); - const assertionCount = getAssertionCount(proposal); - return ( - - Proposal to create new Data Contract from {assertionCount} assertions. - {isExternal && ( - <> - This contract contains external Assertions which will be provisioned in {platformName} - - )} - - ); -}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposalModal.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposalModal.tsx deleted file mode 100644 index 2f5472e532317e..00000000000000 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/proposal/DataContractProposalModal.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { Modal, Button } from 'antd'; -import { DataContractProposalParams } from '../../../../../../../../types.generated'; -import { FreshnessContractSummary } from '../FreshnessContractSummary'; -import { SchemaContractSummary } from '../SchemaContractSummary'; -import { DataQualityContractSummary } from '../DataQualityContractSummary'; -import { ANTD_GRAY } from '../../../../../constants'; - -const NoAssertions = styled.div` - padding: 20px; - font-size: 16px; - color: ${ANTD_GRAY[7]}; -`; - -const ActionButton = styled(Button)``; - -type Props = { - proposal: DataContractProposalParams; - showActions?: boolean; - onClose: () => void; - onApprove?: () => void; - onDeny?: () => void; -}; - -/** - * Displaying a Data Contract Proposal for an entity. - */ -export const DataContractProposalModal = ({ proposal, showActions = true, onClose, onApprove, onDeny }: Props) => { - const hasAssertions = proposal.freshness?.length || proposal.schema?.length || proposal.dataQuality?.length; - - return ( - - {!hasAssertions && Proposal to remove all assertions from Data Contract} - {(proposal.freshness?.length && ) || undefined} - {(proposal.schema?.length && ) || undefined} - {(proposal.dataQuality?.length && ) || - undefined} - {showActions && ( - <> - Approve - Deny - - )} - - ); -}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/fieldDescriptionUtils.ts b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/fieldDescriptionUtils.ts index 9c10e03daa6ed6..3c6e14f1d80abe 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/fieldDescriptionUtils.ts +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/fieldDescriptionUtils.ts @@ -1,14 +1,37 @@ import { AssertionStdOperator, AssertionStdParameters, - // FieldAssertionInfo, - // FieldAssertionType, - // FieldMetricType, - // FieldTransformType, + FieldAssertionInfo, + FieldAssertionType, + FieldMetricType, + FieldTransformType, } from '../../../../../../types.generated'; import { formatNumberWithoutAbbreviation } from '../../../../../shared/formatNumber'; import { parseMaybeStringAsFloatOrDefault } from '../../../../../shared/numberUtil'; -// import { ASSERTION_OPERATOR_TO_DESCRIPTION } from './assertion/profile/summary/shared/constants'; + +const ASSERTION_OPERATOR_TO_DESCRIPTION: Record = { + [AssertionStdOperator.EqualTo]: 'Is equal to', + [AssertionStdOperator.NotEqualTo]: 'Is not equal to', + [AssertionStdOperator.Contain]: 'Contains', + [AssertionStdOperator.RegexMatch]: 'Matches', + [AssertionStdOperator.StartWith]: 'Starts with', + [AssertionStdOperator.EndWith]: 'Ends with', + [AssertionStdOperator.In]: 'Is in', + [AssertionStdOperator.NotIn]: 'Is not in', + + [AssertionStdOperator.IsFalse]: 'Is False', + [AssertionStdOperator.IsTrue]: 'Is True', + [AssertionStdOperator.Null]: 'Is NULL', + [AssertionStdOperator.NotNull]: 'Is not NULL', + + [AssertionStdOperator.GreaterThan]: 'Is greater than', + [AssertionStdOperator.GreaterThanOrEqualTo]: 'Is greater than or equal to', + [AssertionStdOperator.LessThan]: 'Is less than', + [AssertionStdOperator.LessThanOrEqualTo]: 'Is less than or equal to', + [AssertionStdOperator.Between]: 'Is within a range', + + [AssertionStdOperator.Native]: undefined, +}; const SUPPORTED_OPERATORS_FOR_FIELD_DESCRIPTION = [ AssertionStdOperator.EqualTo, @@ -27,12 +50,13 @@ const SUPPORTED_OPERATORS_FOR_FIELD_DESCRIPTION = [ AssertionStdOperator.IsTrue, AssertionStdOperator.IsFalse, ]; -// const getAssertionStdOperator = (operator: AssertionStdOperator) => { -// if (!ASSERTION_OPERATOR_TO_DESCRIPTION[operator] || !SUPPORTED_OPERATORS_FOR_FIELD_DESCRIPTION.includes(operator)) { -// throw new Error(`Unknown operator ${operator}`); -// } -// return ASSERTION_OPERATOR_TO_DESCRIPTION[operator]?.toLowerCase(); -// }; + +const getAssertionStdOperator = (operator: AssertionStdOperator) => { + if (!ASSERTION_OPERATOR_TO_DESCRIPTION[operator] || !SUPPORTED_OPERATORS_FOR_FIELD_DESCRIPTION.includes(operator)) { + throw new Error(`Unknown operator ${operator}`); + } + return ASSERTION_OPERATOR_TO_DESCRIPTION[operator]?.toLowerCase(); +}; export const getFieldMetricTypeReadableLabel = (metric: FieldMetricType) => { switch (metric) { @@ -86,10 +110,16 @@ const getFieldTransformType = (transform: FieldTransformType) => { const getAssertionStdParameters = (parameters: AssertionStdParameters) => { if (parameters.value) { - return formatNumberWithoutAbbreviation(parseMaybeStringAsFloatOrDefault(parameters.value.value, parameters.value.value)); + return formatNumberWithoutAbbreviation( + parseMaybeStringAsFloatOrDefault(parameters.value.value, parameters.value.value), + ); } if (parameters.minValue && parameters.maxValue) { - return `${formatNumberWithoutAbbreviation(parseMaybeStringAsFloatOrDefault(parameters.minValue.value, parameters.minValue.value))} and ${formatNumberWithoutAbbreviation(parseMaybeStringAsFloatOrDefault(parameters.maxValue.value, parameters.maxValue.value))}`; + return `${formatNumberWithoutAbbreviation( + parseMaybeStringAsFloatOrDefault(parameters.minValue.value, parameters.minValue.value), + )} and ${formatNumberWithoutAbbreviation( + parseMaybeStringAsFloatOrDefault(parameters.maxValue.value, parameters.maxValue.value), + )}`; } return ''; }; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx index 7192d9c03da7f3..34d2c4ab9b1d2f 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx @@ -1,7 +1,6 @@ import React from 'react'; import styled from 'styled-components'; import * as moment from 'moment-timezone'; -import * as cronParser from 'cron-parser'; import cronstrue from 'cronstrue'; import { ClockCircleOutlined, diff --git a/datahub-web-react/src/graphql/monitor.graphql b/datahub-web-react/src/graphql/monitor.graphql deleted file mode 100644 index 6e0326f3f65c15..00000000000000 --- a/datahub-web-react/src/graphql/monitor.graphql +++ /dev/null @@ -1,11 +0,0 @@ -fragment assertionDetailsWithMonitors on Assertion { - ...assertionDetails - runEvents(status: COMPLETE, limit: 1) { - total - failed - succeeded - runEvents { - ...assertionRunEventDetails - } - } -} diff --git a/datahub-web-react/yarn.lock b/datahub-web-react/yarn.lock index 642398a3e77d15..0790f03b591232 100644 --- a/datahub-web-react/yarn.lock +++ b/datahub-web-react/yarn.lock @@ -5066,13 +5066,6 @@ create-react-class@^15.6.2: loose-envify "^1.3.1" object-assign "^4.1.1" -cron-parser@^4.8.1: - version "4.9.0" - resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.9.0.tgz#0340694af3e46a0894978c6f52a6dbb5c0f11ad5" - integrity sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q== - dependencies: - luxon "^3.2.1" - cronstrue@^1.122.0: version "1.122.0" resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-1.122.0.tgz#bd6838077b476d28f61d381398b47b8c3912a126" @@ -7570,11 +7563,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -luxon@^3.2.1: - version "3.4.4" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.4.tgz#cf20dc27dc532ba41a169c43fdcc0063601577af" - integrity sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA== - lz-string@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" From 0555c614ee6a6a733c05792c923c1e89a1552ab1 Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Fri, 7 Jun 2024 23:01:44 +0530 Subject: [PATCH 14/26] feat(ui/data-contract): change graphql request to update the status as well --- datahub-web-react/src/graphql/assertion.graphql | 12 ++++++++++++ datahub-web-react/src/graphql/contract.graphql | 6 +++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/datahub-web-react/src/graphql/assertion.graphql b/datahub-web-react/src/graphql/assertion.graphql index 016e8a4f06086b..8b0582b1c5b10c 100644 --- a/datahub-web-react/src/graphql/assertion.graphql +++ b/datahub-web-react/src/graphql/assertion.graphql @@ -93,3 +93,15 @@ query getAssertionRuns($assertionUrn: String!, $startTime: Long, $endTime: Long, mutation deleteAssertion($urn: String!) { deleteAssertion(urn: $urn) } + +fragment assertionDetailsWithRunEvents on Assertion { + ...assertionDetails + runEvents(status: COMPLETE, limit: 1) { + total + failed + succeeded + runEvents { + ...assertionRunEventDetails + } + } +} diff --git a/datahub-web-react/src/graphql/contract.graphql b/datahub-web-react/src/graphql/contract.graphql index 225a63eb599fe6..4c54dba037a350 100644 --- a/datahub-web-react/src/graphql/contract.graphql +++ b/datahub-web-react/src/graphql/contract.graphql @@ -19,17 +19,17 @@ fragment dataContractDetails on DataContract { properties { freshness { assertion { - ...assertionDetails + ...assertionDetailsWithRunEvents } } schema { assertion { - ...assertionDetails + ...assertionDetailsWithRunEvents } } dataQuality { assertion { - ...assertionDetails + ...assertionDetailsWithRunEvents } } } From 21e148a043a95a7b2a5750f557474b1a8f78c4ed Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Mon, 10 Jun 2024 18:04:24 +0530 Subject: [PATCH 15/26] feat(ui/data-contract): removed unwanted code --- .../tabs/Dataset/Validations/DatasetAssertionsList.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx index 3449e47b655ada..f6baac91f02d31 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx @@ -144,9 +144,6 @@ export const DatasetAssertionsList = ({ const resultIcon = (record.lastExecResult && getResultIcon(record.lastExecResult)) || ; const selected = selectedUrns?.some((selectedUrn) => selectedUrn === record.urn); const isPartOfContract = contract && isAssertionPartOfContract(record, contract); - const assertionEntityUrn = record.urn; - console.log('isPartOfContract>>>>>', isPartOfContract); - console.log('assertionEntityUrn>>>>>', assertionEntityUrn); const { description } = record; return ( @@ -170,9 +167,7 @@ export const DatasetAssertionsList = ({ description={description} assertionInfo={record.datasetAssertionInfo} /> - {/* TODO work on below condition to handle assertionEntityUrn */} - {/* {(isPartOfContract && assertionEntityUrn && ( */} - {(isPartOfContract && ( + {(isPartOfContract && entityData?.urn && ( From 0bd62b18089145e9849cad670a971d7d84055466 Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Tue, 11 Jun 2024 15:14:39 +0530 Subject: [PATCH 16/26] feat(ui/data-contract): resolved PR comments --- datahub-web-react/package.json | 1 - .../Validations/DatasetAssertionsList.tsx | 74 +++++++++---------- .../Validations/FieldAssertionDescription.tsx | 1 - .../Dataset/Validations/ValidationsTab.tsx | 28 +++---- .../contract/DataContractSummary.tsx | 6 +- .../shared/tabs/Dataset/Validations/utils.tsx | 26 ------- datahub-web-react/yarn.lock | 19 ----- 7 files changed, 53 insertions(+), 102 deletions(-) diff --git a/datahub-web-react/package.json b/datahub-web-react/package.json index 553a7f178a390b..0cf2c1642a072a 100644 --- a/datahub-web-react/package.json +++ b/datahub-web-react/package.json @@ -11,7 +11,6 @@ "@apollo/client": "^3.3.19", "@graphql-codegen/fragment-matcher": "^5.0.0", "@monaco-editor/react": "^4.3.1", - "@mui/icons-material": "^5.15.2", "@react-hook/window-size": "^3.0.7", "@react-spring/web": "^9.7.3", "@remirror/pm": "^2.0.3", diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx index f6baac91f02d31..14edc712205d7f 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx @@ -203,44 +203,42 @@ export const DatasetAssertionsList = ({ title: '', dataIndex: '', key: '', - render: (_, record: any) => { - return ( - <> - {showMenu && ( - - - - {(record.platform.properties?.logoUrl && ( - - )) || ( - - {record.platform.properties?.displayName || - capitalizeFirstLetterOnly(record.platform.name)} - - )} - - - - } trigger={['click']}> - - - - )} - - ); - }, + render: (_, record: any) => ( + <> + {showMenu && ( + + + + {(record.platform.properties?.logoUrl && ( + + )) || ( + + {record.platform.properties?.displayName || + capitalizeFirstLetterOnly(record.platform.name)} + + )} + + + + } trigger={['click']}> + + + + )} + + ), }, ]; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/FieldAssertionDescription.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/FieldAssertionDescription.tsx index f57a1be6f66e13..a073f25b493074 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/FieldAssertionDescription.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/FieldAssertionDescription.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { Typography } from 'antd'; -// import { FieldAssertionInfo } from '../../../../../../types.generated'; import { getFieldDescription, getFieldOperatorDescription, diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/ValidationsTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/ValidationsTab.tsx index c4e9084d67c7a4..7615583047800a 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/ValidationsTab.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/ValidationsTab.tsx @@ -81,25 +81,25 @@ export const ValidationsTab = () => { }, ]; - // if (appConfig.config.featureFlags?.dataContractsEnabled) { - // If contracts feature is enabled, add to list. + if (appConfig.config.featureFlags?.dataContractsEnabled) { + // If contracts feature is enabled, add to list. - tabs.push({ - title: ( - <> - + tabs.push({ + title: ( + <> + - Data Contract - - ), + Data Contract + + ), - path: TabPaths.DATA_CONTRACT, + path: TabPaths.DATA_CONTRACT, - content: , + content: , - disabled: false, - }); - // } + disabled: false, + }); + } return ( <> diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummary.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummary.tsx index c280c4d6eecd0b..9b684486cb5ce7 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummary.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractSummary.tsx @@ -1,6 +1,6 @@ import React from 'react'; import styled from 'styled-components'; -import EditIcon from '@mui/icons-material/Edit'; +import { EditFilled } from '@ant-design/icons'; import { Button, Typography } from 'antd'; import { DataContractState } from '../../../../../../../types.generated'; import { AssertionStatusSummary } from '../types'; @@ -57,9 +57,9 @@ const CreateButton = styled(Button)` } `; -const EditIconStyle = styled(EditIcon)` +const EditIconStyle = styled(EditFilled)` && { - font-size: 16px; + font-size: 12px; } `; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx index 34d2c4ab9b1d2f..8cf9678ade29f2 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx @@ -261,7 +261,6 @@ export const createAssertionGroups = (assertions: Array): AssertionGr return assertionGroups; }; -// TODO: Make this the default inside DatasetAssertionsSummary.tsx. export const getAssertionGroupSummaryIcon = (summary: AssertionStatusSummary) => { if (summary.total === 0) { return null; @@ -275,7 +274,6 @@ export const getAssertionGroupSummaryIcon = (summary: AssertionStatusSummary) => return ; }; -// TODO: Make this the default inside DatasetAssertionsSummary.tsx. export const getAssertionGroupSummaryMessage = (summary: AssertionStatusSummary) => { if (summary.total === 0) { return 'No assertions have run'; @@ -298,27 +296,3 @@ export const getAssertionTypesForEntityType = (entityType: EntityType, monitorsC enabled: type.enabled && (!type.requiresConnectionSupportedByMonitors || monitorsConnectionForEntityExists), })); }; - -// export const getEntityUrnForAssertion = (assertion: Assertion) => { -export const getEntityUrnForAssertion = (assertion: any) => { - if (assertion.info?.type === AssertionType.Dataset) { - return assertion.info?.datasetAssertion?.datasetUrn; - } - if (assertion.info?.type === AssertionType.Freshness) { - return assertion.info?.freshnessAssertion?.entityUrn; - } - if (assertion.info?.type === AssertionType.Volume) { - return assertion.info?.volumeAssertion?.entityUrn; - } - if (assertion.info?.type === AssertionType.Field) { - return assertion.info?.fieldAssertion?.entityUrn; - } - if (assertion.info?.type === AssertionType.Sql) { - return assertion.info?.sqlAssertion?.entityUrn; - } - if (assertion.info?.type === AssertionType.DataSchema) { - return assertion.info?.schemaAssertion?.entityUrn; - } - console.error(`Unable to extract entity urn from unrecognized assertion with type ${assertion.info?.type}`); - return undefined; -}; diff --git a/datahub-web-react/yarn.lock b/datahub-web-react/yarn.lock index 0790f03b591232..702e499b2821ee 100644 --- a/datahub-web-react/yarn.lock +++ b/datahub-web-react/yarn.lock @@ -825,13 +825,6 @@ dependencies: regenerator-runtime "^0.13.11" -"@babel/runtime@^7.23.9": - version "7.24.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.6.tgz#5b76eb89ad45e2e4a0a8db54c456251469a3358e" - integrity sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw== - dependencies: - regenerator-runtime "^0.14.0" - "@babel/template@^7.18.10", "@babel/template@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" @@ -1762,13 +1755,6 @@ resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.15.tgz#490f3dea5327c892f063496a0219c48301da0fa0" integrity sha512-xFcS0LpdF0Q1qJrrNsYUv9PU+ovvhCEPTOMw2jcpEFtl3CA87dLpvztORR5oE2UBFjWF7qLQLOwboQU1+xC7Cw== -"@mui/icons-material@^5.15.2": - version "5.15.19" - resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.15.19.tgz#0602da80d814af662812659eab891e435ec0d5c0" - integrity sha512-RsEiRxA5azN9b8gI7JRqekkgvxQUlitoBOtZglflb8cUDyP12/cP4gRwhb44Ea1/zwwGGjAj66ZJpGHhKfibNA== - dependencies: - "@babel/runtime" "^7.23.9" - "@mui/material@^5.8.5": version "5.10.15" resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.10.15.tgz#37345f5a3d71c662703af7b5be0cca229b2a1416" @@ -9400,11 +9386,6 @@ regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== -regenerator-runtime@^0.14.0: - version "0.14.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" - integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== - regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" From b1278f99307bd045ba1e913a1ed13e5995efdb47 Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Tue, 11 Jun 2024 15:15:14 +0530 Subject: [PATCH 17/26] feat(ui/data-contract): resolved PR comments --- .../configuration/src/main/resources/application.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/metadata-service/configuration/src/main/resources/application.yaml b/metadata-service/configuration/src/main/resources/application.yaml index 4d188bd5c61839..e9dcb1b94174ad 100644 --- a/metadata-service/configuration/src/main/resources/application.yaml +++ b/metadata-service/configuration/src/main/resources/application.yaml @@ -370,6 +370,7 @@ featureFlags: nestedDomainsEnabled: ${NESTED_DOMAINS_ENABLED:true} # Enables the nested Domains feature that allows users to have sub-Domains. If this is off, Domains appear "flat" again schemaFieldEntityFetchEnabled: ${SCHEMA_FIELD_ENTITY_FETCH_ENABLED:true} # Enables fetching for schema field entities from the database when we hydrate them on schema fields businessAttributeEntityEnabled: ${BUSINESS_ATTRIBUTE_ENTITY_ENABLED:false} # Enables business attribute entity which can be associated with field of dataset + dataContractsEnabled: ${DATA_CONTRACTS_ENABLED:true} # Enables the Data Contracts feature (Tab) in the UI entityChangeEvents: enabled: ${ENABLE_ENTITY_CHANGE_EVENTS_HOOK:true} @@ -476,7 +477,7 @@ metadataChangeProposal: maxAttempts: ${MCP_VERSIONED_MAX_ATTEMPTS:1000} initialIntervalMs: ${MCP_VERSIONED_INITIAL_INTERVAL_MS:100} multiplier: ${MCP_VERSIONED_MULTIPLIER:10} - maxIntervalMs: ${MCP_VERSIONED_MAX_INTERVAL_MS:30000} + maxIntervalMs: ${MCP_VERSIONED_MAX_INTERVAL_MS:30000} # Timeseries MCL topic timeseries: # Whether to throttle MCP processing based on MCL backlog @@ -485,4 +486,4 @@ metadataChangeProposal: maxAttempts: ${MCP_TIMESERIES_MAX_ATTEMPTS:1000} initialIntervalMs: ${MCP_TIMESERIES_INITIAL_INTERVAL_MS:100} multiplier: ${MCP_TIMESERIES_MULTIPLIER:10} - maxIntervalMs: ${MCP_TIMESERIES_MAX_INTERVAL_MS:30000} \ No newline at end of file + maxIntervalMs: ${MCP_TIMESERIES_MAX_INTERVAL_MS:30000} From 364d508e6075f49e0ab09a208ffa00668db526d0 Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Wed, 12 Jun 2024 17:52:32 +0530 Subject: [PATCH 18/26] feat(ui/data-contract): resolved PR comments --- datahub-web-react/package.json | 2 +- .../Validations/FieldAssertionDescription.tsx | 3 +- .../VolumeAssertionDescription.tsx | 44 +++++++++++++++++-- .../contract/DataContractEmptyState.tsx | 3 -- .../Validations/contract/DataContractTab.tsx | 11 ++--- .../contract/DataQualityContractSummary.tsx | 6 +-- .../contract/FreshnessScheduleSummary.tsx | 20 ++++----- .../DataContractAssertionGroupSelect.tsx | 5 +-- .../contract/builder/DataContractBuilder.tsx | 16 +------ .../shared/tabs/Dataset/Validations/utils.tsx | 14 ------ datahub-web-react/src/appConfigContext.tsx | 2 +- 11 files changed, 67 insertions(+), 59 deletions(-) diff --git a/datahub-web-react/package.json b/datahub-web-react/package.json index 0cf2c1642a072a..50a74bb0f42593 100644 --- a/datahub-web-react/package.json +++ b/datahub-web-react/package.json @@ -150,4 +150,4 @@ "prosemirror-transform": "1.2.2", "prosemirror-view": "1.13.4" } -} \ No newline at end of file +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/FieldAssertionDescription.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/FieldAssertionDescription.tsx index a073f25b493074..a104903dc7bc2c 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/FieldAssertionDescription.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/FieldAssertionDescription.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Typography } from 'antd'; +import { FieldAssertionInfo } from '../../../../../../types.generated'; import { getFieldDescription, getFieldOperatorDescription, @@ -8,7 +9,7 @@ import { } from './fieldDescriptionUtils'; type Props = { - assertionInfo: any; + assertionInfo: FieldAssertionInfo; }; /** diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/VolumeAssertionDescription.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/VolumeAssertionDescription.tsx index 2f8fe9c1a70aa1..6ee3bb77a8ce06 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/VolumeAssertionDescription.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/VolumeAssertionDescription.tsx @@ -9,7 +9,6 @@ import { VolumeAssertionInfo, VolumeAssertionType, } from '../../../../../../types.generated'; -import { getIsRowCountChange, getVolumeTypeInfo } from './assertion/builder/steps/volume/utils'; import { formatNumberWithoutAbbreviation } from '../../../../../shared/formatNumber'; import { parseMaybeStringAsFloatOrDefault } from '../../../../../shared/numberUtil'; @@ -17,6 +16,39 @@ type Props = { assertionInfo: VolumeAssertionInfo; }; +export type VolumeTypeField = + | 'rowCountTotal' + | 'rowCountChange' + | 'incrementingSegmentRowCountTotal' + | 'incrementingSegmentRowCountChange'; + +export const getPropertyFromVolumeType = (type: VolumeAssertionType) => { + switch (type) { + case VolumeAssertionType.RowCountTotal: + return 'rowCountTotal' as VolumeTypeField; + case VolumeAssertionType.RowCountChange: + return 'rowCountChange' as VolumeTypeField; + case VolumeAssertionType.IncrementingSegmentRowCountTotal: + return 'incrementingSegmentRowCountTotal' as VolumeTypeField; + case VolumeAssertionType.IncrementingSegmentRowCountChange: + return 'incrementingSegmentRowCountChange' as VolumeTypeField; + default: + throw new Error(`Unknown volume assertion type: ${type}`); + } +}; + +export const getVolumeTypeInfo = (volumeAssertion: VolumeAssertionInfo) => { + const result = volumeAssertion[getPropertyFromVolumeType(volumeAssertion.type)]; + if (!result) { + return undefined; + } + return result; +}; + +export const getIsRowCountChange = (type: VolumeAssertionType) => { + return [VolumeAssertionType.RowCountChange, VolumeAssertionType.IncrementingSegmentRowCountChange].includes(type); +}; + const getVolumeTypeDescription = (volumeType: VolumeAssertionType) => { switch (volumeType) { case VolumeAssertionType.RowCountTotal: @@ -56,10 +88,16 @@ const getValueChangeTypeDescription = (valueChangeType: AssertionValueChangeType const getParameterDescription = (parameters: AssertionStdParameters) => { if (parameters.value) { - return formatNumberWithoutAbbreviation(parseMaybeStringAsFloatOrDefault(parameters.value.value, parameters.value.value)); + return formatNumberWithoutAbbreviation( + parseMaybeStringAsFloatOrDefault(parameters.value.value, parameters.value.value), + ); } if (parameters.minValue && parameters.maxValue) { - return `${formatNumberWithoutAbbreviation(parseMaybeStringAsFloatOrDefault(parameters.minValue.value, parameters.minValue.value))} and ${formatNumberWithoutAbbreviation(parseMaybeStringAsFloatOrDefault(parameters.maxValue.value, parameters.maxValue.value))}`; + return `${formatNumberWithoutAbbreviation( + parseMaybeStringAsFloatOrDefault(parameters.minValue.value, parameters.minValue.value), + )} and ${formatNumberWithoutAbbreviation( + parseMaybeStringAsFloatOrDefault(parameters.maxValue.value, parameters.maxValue.value), + )}`; } throw new Error('Invalid assertion parameters provided'); }; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState.tsx index 1ddeaf22eae331..043d89b7cd7c2c 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState.tsx @@ -58,9 +58,6 @@ type Props = { showContractBuilder: () => void; }; -/** - * Displaying a Data Contract proposal for an entity. - */ export const DataContractEmptyState = ({ showContractBuilder }: Props) => { return ( diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx index 87438d59cd3bad..98cb9223fe8a9d 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx @@ -68,7 +68,7 @@ export const DataContractTab = () => { return ( <> - {(data?.dataset?.contract && ( + {data?.dataset?.contract ? ( <> { )} - {(hasDataQualityContract && ( + {hasDataQualityContract ? ( - )) || - undefined} + ) : undefined} - )) || setShowContractBuilder(true)} />} + ) : ( + setShowContractBuilder(true)} /> + )} {showContractBuilder && ( )} - {/* {assertion.info?.volumeAssertion && ( + {assertion.info?.volumeAssertion && ( - )} */} + )} {assertion.info?.fieldAssertion && ( )} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/FreshnessScheduleSummary.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/FreshnessScheduleSummary.tsx index 9dfb7216c58727..5ed94237a13f31 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/FreshnessScheduleSummary.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/FreshnessScheduleSummary.tsx @@ -1,20 +1,20 @@ import React from 'react'; import cronstrue from 'cronstrue'; -// import { -// FreshnessAssertionSchedule, -// FreshnessAssertionScheduleType, -// CronSchedule, -// } from '../../../../../../../types.generated'; +import { + FreshnessAssertionSchedule, + FreshnessAssertionScheduleType, + CronSchedule, +} from '../../../../../../../types.generated'; import { capitalizeFirstLetter } from '../../../../../../shared/textUtil'; -// type Props = { -// definition: FreshnessAssertionSchedule; -// evaluationSchedule?: CronSchedule; // When the assertion is run. -// }; +type Props = { + definition: FreshnessAssertionSchedule; + evaluationSchedule?: CronSchedule; // When the assertion is run. +}; export const FreshnessScheduleSummary = ({ definition, evaluationSchedule }: any) => { const scheduleText = - definition.type === 'CRON' //FreshnessAssertionScheduleType.Cron + definition.type === FreshnessAssertionScheduleType.Cron ? `${capitalizeFirstLetter(cronstrue.toString(definition.cron?.cron as string))}.` : `In the past ${ definition.fixedInterval?.multiple diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractAssertionGroupSelect.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractAssertionGroupSelect.tsx index 4dcfdf1fffceca..f96149dd0be5ef 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractAssertionGroupSelect.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractAssertionGroupSelect.tsx @@ -40,7 +40,7 @@ export const DataContractAssertionGroupSelect = ({ return ( <> - {category} {!multiple && `(Choose 1)`} + {category} {!multiple ? `(Choose 1)` : ''} ); diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx index 78e0f65db660fc..e13ff6aab9bcbb 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx @@ -2,26 +2,15 @@ import React, { useState } from 'react'; import { message, Button, Tooltip } from 'antd'; import styled from 'styled-components'; import lodash from 'lodash'; -import { - DataContract, - AssertionType, - // DataContractProposalOperationType, - // ActionRequestType, - EntityType, - Assertion, -} from '../../../../../../../../types.generated'; +import { DataContract, AssertionType, EntityType, Assertion } from '../../../../../../../../types.generated'; import { DataContractBuilderState, DataContractCategoryType, DEFAULT_BUILDER_STATE } from './types'; import { buildUpsertDataContractMutationVariables } from './utils'; -import { - // useProposeDataContractMutation, - useUpsertDataContractMutation, -} from '../../../../../../../../graphql/contract.generated'; +import { useUpsertDataContractMutation } from '../../../../../../../../graphql/contract.generated'; import { createAssertionGroups } from '../../utils'; import { DataContractAssertionGroupSelect } from './DataContractAssertionGroupSelect'; import { ANTD_GRAY } from '../../../../../constants'; import { DATA_QUALITY_ASSERTION_TYPES } from '../utils'; import { useGetDatasetAssertionsQuery } from '../../../../../../../../graphql/dataset.generated'; -// import analytics, { EntityActionType, EventType } from '../../../../../../../analytics'; const AssertionsSection = styled.div` border: 0.5px solid ${ANTD_GRAY[4]}; @@ -69,7 +58,6 @@ export const DataContractBuilder = ({ entityUrn, entityType, initialState, onSub const isEdit = !!initialState; const [builderState, setBuilderState] = useState(initialState || DEFAULT_BUILDER_STATE); const [upsertDataContractMutation] = useUpsertDataContractMutation(); - // const [proposeDataContractMutation] = useProposeDataContractMutation(); // note that for contracts, we do not allow the use of sibling node assertions, for clarity. const { data } = useGetDatasetAssertionsQuery({ diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx index 8cf9678ade29f2..c5d44864230a72 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx @@ -209,20 +209,6 @@ export const getAssertionsSummary = (assertions: Assertion[]): AssertionStatusSu return summary; }; -/** - * TODO: We will remove this mapping code once we replace the OSS legacy assertions summary with the new - * format. - */ -export const getLegacyAssertionsSummary = (assertions: Assertion[]) => { - const newSummary = getAssertionsSummary(assertions); - return { - failedRuns: newSummary.failing, - succeededRuns: newSummary.passing, - erroredRuns: newSummary.erroring, - totalRuns: newSummary.total, - totalAssertions: newSummary.totalAssertions, - }; -}; // /** // * Returns a list of assertion groups, where assertions are grouped diff --git a/datahub-web-react/src/appConfigContext.tsx b/datahub-web-react/src/appConfigContext.tsx index 8f7bb0e0b5e47c..4262f772b006ed 100644 --- a/datahub-web-react/src/appConfigContext.tsx +++ b/datahub-web-react/src/appConfigContext.tsx @@ -53,7 +53,7 @@ export const DEFAULT_APP_CONFIG = { nestedDomainsEnabled: true, platformBrowseV2: false, businessAttributeEntityEnabled: false, - dataContractsEnabled: true, + dataContractsEnabled: false, }, }; From c8e71e70cf62c70786d678de5029e300aceea047 Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Thu, 13 Jun 2024 03:09:06 +0530 Subject: [PATCH 19/26] feat(ui/data-contract): resolved PR comments --- .../entity/shared/tabs/Dataset/Validations/Assertions.tsx | 8 +++++++- .../Dataset/Validations/VolumeAssertionDescription.tsx | 8 ++++---- .../Validations/contract/FreshnessScheduleSummary.tsx | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx index 86d4b5234c632f..57c670e334e2b6 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx @@ -16,6 +16,7 @@ const getAssertionsStatusSummary = (assertions: Array) => { failedRuns: 0, succeededRuns: 0, totalRuns: 0, + erroredRuns: 0, totalAssertions: assertions.length, }; assertions.forEach((assertion) => { @@ -28,7 +29,12 @@ const getAssertionsStatusSummary = (assertions: Array) => { if (AssertionResultType.Failure === resultType) { summary.failedRuns++; } - summary.totalRuns++; // only count assertions for which there is one completed run event! + if (AssertionResultType.Error === resultType) { + summary.erroredRuns++; + } + if (AssertionResultType.Init !== resultType) { + summary.totalRuns++; // only count assertions for which there is one completed run event, ignoring INIT statuses! + } } }); return summary; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/VolumeAssertionDescription.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/VolumeAssertionDescription.tsx index 6ee3bb77a8ce06..fbe010a270a1d7 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/VolumeAssertionDescription.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/VolumeAssertionDescription.tsx @@ -16,13 +16,13 @@ type Props = { assertionInfo: VolumeAssertionInfo; }; -export type VolumeTypeField = +type VolumeTypeField = | 'rowCountTotal' | 'rowCountChange' | 'incrementingSegmentRowCountTotal' | 'incrementingSegmentRowCountChange'; -export const getPropertyFromVolumeType = (type: VolumeAssertionType) => { +const getPropertyFromVolumeType = (type: VolumeAssertionType) => { switch (type) { case VolumeAssertionType.RowCountTotal: return 'rowCountTotal' as VolumeTypeField; @@ -37,7 +37,7 @@ export const getPropertyFromVolumeType = (type: VolumeAssertionType) => { } }; -export const getVolumeTypeInfo = (volumeAssertion: VolumeAssertionInfo) => { +const getVolumeTypeInfo = (volumeAssertion: VolumeAssertionInfo) => { const result = volumeAssertion[getPropertyFromVolumeType(volumeAssertion.type)]; if (!result) { return undefined; @@ -45,7 +45,7 @@ export const getVolumeTypeInfo = (volumeAssertion: VolumeAssertionInfo) => { return result; }; -export const getIsRowCountChange = (type: VolumeAssertionType) => { +const getIsRowCountChange = (type: VolumeAssertionType) => { return [VolumeAssertionType.RowCountChange, VolumeAssertionType.IncrementingSegmentRowCountChange].includes(type); }; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/FreshnessScheduleSummary.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/FreshnessScheduleSummary.tsx index 5ed94237a13f31..434ccb985574f7 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/FreshnessScheduleSummary.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/FreshnessScheduleSummary.tsx @@ -12,7 +12,7 @@ type Props = { evaluationSchedule?: CronSchedule; // When the assertion is run. }; -export const FreshnessScheduleSummary = ({ definition, evaluationSchedule }: any) => { +export const FreshnessScheduleSummary = ({ definition, evaluationSchedule }: Props) => { const scheduleText = definition.type === FreshnessAssertionScheduleType.Cron ? `${capitalizeFirstLetter(cronstrue.toString(definition.cron?.cron as string))}.` From 036cdf3c79590363e2f23bb84d999e4b5b8471fd Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Fri, 14 Jun 2024 13:15:07 +0530 Subject: [PATCH 20/26] feat(ui/data-contract): create util function for the volume --- .../Dataset/Validations/ValidationsTab.tsx | 4 - .../VolumeAssertionDescription.tsx | 100 ++--------------- .../shared/tabs/Dataset/Validations/utils.tsx | 101 +++++++++++++++++- 3 files changed, 107 insertions(+), 98 deletions(-) diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/ValidationsTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/ValidationsTab.tsx index 7615583047800a..92af9bfc2b567b 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/ValidationsTab.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/ValidationsTab.tsx @@ -83,7 +83,6 @@ export const ValidationsTab = () => { if (appConfig.config.featureFlags?.dataContractsEnabled) { // If contracts feature is enabled, add to list. - tabs.push({ title: ( <> @@ -92,11 +91,8 @@ export const ValidationsTab = () => { Data Contract ), - path: TabPaths.DATA_CONTRACT, - content: , - disabled: false, }); } diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/VolumeAssertionDescription.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/VolumeAssertionDescription.tsx index fbe010a270a1d7..26634c459df0d4 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/VolumeAssertionDescription.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/VolumeAssertionDescription.tsx @@ -1,107 +1,23 @@ import React from 'react'; import { Typography } from 'antd'; import { - AssertionStdOperator, - AssertionStdParameters, - AssertionValueChangeType, IncrementingSegmentRowCountChange, RowCountChange, VolumeAssertionInfo, - VolumeAssertionType, } from '../../../../../../types.generated'; -import { formatNumberWithoutAbbreviation } from '../../../../../shared/formatNumber'; -import { parseMaybeStringAsFloatOrDefault } from '../../../../../shared/numberUtil'; +import { + getIsRowCountChange, + getOperatorDescription, + getParameterDescription, + getValueChangeTypeDescription, + getVolumeTypeDescription, + getVolumeTypeInfo, +} from './utils'; type Props = { assertionInfo: VolumeAssertionInfo; }; -type VolumeTypeField = - | 'rowCountTotal' - | 'rowCountChange' - | 'incrementingSegmentRowCountTotal' - | 'incrementingSegmentRowCountChange'; - -const getPropertyFromVolumeType = (type: VolumeAssertionType) => { - switch (type) { - case VolumeAssertionType.RowCountTotal: - return 'rowCountTotal' as VolumeTypeField; - case VolumeAssertionType.RowCountChange: - return 'rowCountChange' as VolumeTypeField; - case VolumeAssertionType.IncrementingSegmentRowCountTotal: - return 'incrementingSegmentRowCountTotal' as VolumeTypeField; - case VolumeAssertionType.IncrementingSegmentRowCountChange: - return 'incrementingSegmentRowCountChange' as VolumeTypeField; - default: - throw new Error(`Unknown volume assertion type: ${type}`); - } -}; - -const getVolumeTypeInfo = (volumeAssertion: VolumeAssertionInfo) => { - const result = volumeAssertion[getPropertyFromVolumeType(volumeAssertion.type)]; - if (!result) { - return undefined; - } - return result; -}; - -const getIsRowCountChange = (type: VolumeAssertionType) => { - return [VolumeAssertionType.RowCountChange, VolumeAssertionType.IncrementingSegmentRowCountChange].includes(type); -}; - -const getVolumeTypeDescription = (volumeType: VolumeAssertionType) => { - switch (volumeType) { - case VolumeAssertionType.RowCountTotal: - case VolumeAssertionType.IncrementingSegmentRowCountTotal: - return 'has'; - case VolumeAssertionType.RowCountChange: - case VolumeAssertionType.IncrementingSegmentRowCountChange: - return 'should grow by'; - default: - throw new Error(`Unknown volume type ${volumeType}`); - } -}; - -const getOperatorDescription = (operator: AssertionStdOperator) => { - switch (operator) { - case AssertionStdOperator.GreaterThanOrEqualTo: - return 'at least'; - case AssertionStdOperator.LessThanOrEqualTo: - return 'at most'; - case AssertionStdOperator.Between: - return 'between'; - default: - throw new Error(`Unknown operator ${operator}`); - } -}; - -const getValueChangeTypeDescription = (valueChangeType: AssertionValueChangeType) => { - switch (valueChangeType) { - case AssertionValueChangeType.Absolute: - return 'rows'; - case AssertionValueChangeType.Percentage: - return '%'; - default: - throw new Error(`Unknown value change type ${valueChangeType}`); - } -}; - -const getParameterDescription = (parameters: AssertionStdParameters) => { - if (parameters.value) { - return formatNumberWithoutAbbreviation( - parseMaybeStringAsFloatOrDefault(parameters.value.value, parameters.value.value), - ); - } - if (parameters.minValue && parameters.maxValue) { - return `${formatNumberWithoutAbbreviation( - parseMaybeStringAsFloatOrDefault(parameters.minValue.value, parameters.minValue.value), - )} and ${formatNumberWithoutAbbreviation( - parseMaybeStringAsFloatOrDefault(parameters.maxValue.value, parameters.maxValue.value), - )}`; - } - throw new Error('Invalid assertion parameters provided'); -}; - /** * A human-readable description of a Volume Assertion. */ diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx index c5d44864230a72..5358ed07589bc4 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx @@ -13,10 +13,22 @@ import { CodeOutlined, ExclamationCircleOutlined, } from '@ant-design/icons'; -import { Assertion, AssertionResultType, AssertionType, EntityType } from '../../../../../../types.generated'; +import { + Assertion, + AssertionResultType, + AssertionStdOperator, + AssertionStdParameters, + AssertionType, + AssertionValueChangeType, + EntityType, + VolumeAssertionInfo, + VolumeAssertionType, +} from '../../../../../../types.generated'; import { sortAssertions } from './assertionUtils'; import { AssertionGroup, AssertionStatusSummary } from './types'; import { lowerFirstLetter } from '../../../../../shared/textUtil'; +import { formatNumberWithoutAbbreviation } from '../../../../../shared/formatNumber'; +import { parseMaybeStringAsFloatOrDefault } from '../../../../../shared/numberUtil'; export const SUCCESS_COLOR_HEX = '#52C41A'; export const FAILURE_COLOR_HEX = '#F5222D'; @@ -209,7 +221,6 @@ export const getAssertionsSummary = (assertions: Assertion[]): AssertionStatusSu return summary; }; - // /** // * Returns a list of assertion groups, where assertions are grouped // * by their "type" or "category". Each group includes the assertions inside, along with @@ -282,3 +293,89 @@ export const getAssertionTypesForEntityType = (entityType: EntityType, monitorsC enabled: type.enabled && (!type.requiresConnectionSupportedByMonitors || monitorsConnectionForEntityExists), })); }; + +type VolumeTypeField = + | 'rowCountTotal' + | 'rowCountChange' + | 'incrementingSegmentRowCountTotal' + | 'incrementingSegmentRowCountChange'; + +export const getPropertyFromVolumeType = (type: VolumeAssertionType) => { + switch (type) { + case VolumeAssertionType.RowCountTotal: + return 'rowCountTotal' as VolumeTypeField; + case VolumeAssertionType.RowCountChange: + return 'rowCountChange' as VolumeTypeField; + case VolumeAssertionType.IncrementingSegmentRowCountTotal: + return 'incrementingSegmentRowCountTotal' as VolumeTypeField; + case VolumeAssertionType.IncrementingSegmentRowCountChange: + return 'incrementingSegmentRowCountChange' as VolumeTypeField; + default: + throw new Error(`Unknown volume assertion type: ${type}`); + } +}; + +export const getVolumeTypeInfo = (volumeAssertion: VolumeAssertionInfo) => { + const result = volumeAssertion[getPropertyFromVolumeType(volumeAssertion.type)]; + if (!result) { + return undefined; + } + return result; +}; + +export const getIsRowCountChange = (type: VolumeAssertionType) => { + return [VolumeAssertionType.RowCountChange, VolumeAssertionType.IncrementingSegmentRowCountChange].includes(type); +}; + +export const getVolumeTypeDescription = (volumeType: VolumeAssertionType) => { + switch (volumeType) { + case VolumeAssertionType.RowCountTotal: + case VolumeAssertionType.IncrementingSegmentRowCountTotal: + return 'has'; + case VolumeAssertionType.RowCountChange: + case VolumeAssertionType.IncrementingSegmentRowCountChange: + return 'should grow by'; + default: + throw new Error(`Unknown volume type ${volumeType}`); + } +}; + +export const getOperatorDescription = (operator: AssertionStdOperator) => { + switch (operator) { + case AssertionStdOperator.GreaterThanOrEqualTo: + return 'at least'; + case AssertionStdOperator.LessThanOrEqualTo: + return 'at most'; + case AssertionStdOperator.Between: + return 'between'; + default: + throw new Error(`Unknown operator ${operator}`); + } +}; + +export const getValueChangeTypeDescription = (valueChangeType: AssertionValueChangeType) => { + switch (valueChangeType) { + case AssertionValueChangeType.Absolute: + return 'rows'; + case AssertionValueChangeType.Percentage: + return '%'; + default: + throw new Error(`Unknown value change type ${valueChangeType}`); + } +}; + +export const getParameterDescription = (parameters: AssertionStdParameters) => { + if (parameters.value) { + return formatNumberWithoutAbbreviation( + parseMaybeStringAsFloatOrDefault(parameters.value.value, parameters.value.value), + ); + } + if (parameters.minValue && parameters.maxValue) { + return `${formatNumberWithoutAbbreviation( + parseMaybeStringAsFloatOrDefault(parameters.minValue.value, parameters.minValue.value), + )} and ${formatNumberWithoutAbbreviation( + parseMaybeStringAsFloatOrDefault(parameters.maxValue.value, parameters.maxValue.value), + )}`; + } + throw new Error('Invalid assertion parameters provided'); +}; From f74169a25ac77bcba6d92f99f93a8bef0c0c9410 Mon Sep 17 00:00:00 2001 From: jayacryl <159848059+jayacryl@users.noreply.github.com> Date: Fri, 14 Jun 2024 10:25:40 -0400 Subject: [PATCH 21/26] feat(feature-flags): data contract feature flag --- .../linkedin/datahub/graphql/featureflags/FeatureFlags.java | 1 + .../datahub/graphql/resolvers/config/AppConfigResolver.java | 1 + datahub-graphql-core/src/main/resources/app.graphql | 5 +++++ datahub-web-react/src/graphql/app.graphql | 1 + .../configuration/src/main/resources/application.yaml | 1 + 5 files changed, 9 insertions(+) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/featureflags/FeatureFlags.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/featureflags/FeatureFlags.java index 2a4f75cf6055a0..2a9af37a06ad9e 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/featureflags/FeatureFlags.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/featureflags/FeatureFlags.java @@ -20,4 +20,5 @@ public class FeatureFlags { private boolean nestedDomainsEnabled = false; private boolean schemaFieldEntityFetchEnabled = false; private boolean businessAttributeEntityEnabled = false; + private boolean dataContractsEnabled = false; } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java index c05009e146308e..caa469003c22e2 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java @@ -185,6 +185,7 @@ public CompletableFuture get(final DataFetchingEnvironment environmen .setShowAccessManagement(_featureFlags.isShowAccessManagement()) .setNestedDomainsEnabled(_featureFlags.isNestedDomainsEnabled()) .setPlatformBrowseV2(_featureFlags.isPlatformBrowseV2()) + .setDataContractsEnabled(_featureFlags.isDataContractsEnabled()) .build(); appConfig.setFeatureFlags(featureFlagsConfig); diff --git a/datahub-graphql-core/src/main/resources/app.graphql b/datahub-graphql-core/src/main/resources/app.graphql index d84a86a3bedd36..b3a965981c366a 100644 --- a/datahub-graphql-core/src/main/resources/app.graphql +++ b/datahub-graphql-core/src/main/resources/app.graphql @@ -492,6 +492,11 @@ type FeatureFlagsConfig { Whether business attribute entity should be shown """ businessAttributeEntityEnabled: Boolean! + + """ + Whether data contracts should be enabled + """ + dataContractsEnabled: Boolean! } """ diff --git a/datahub-web-react/src/graphql/app.graphql b/datahub-web-react/src/graphql/app.graphql index 7b47fc0302247b..662b2f11336fa9 100644 --- a/datahub-web-react/src/graphql/app.graphql +++ b/datahub-web-react/src/graphql/app.graphql @@ -68,6 +68,7 @@ query appConfig { nestedDomainsEnabled platformBrowseV2 businessAttributeEntityEnabled + dataContractsEnabled } } } diff --git a/metadata-service/configuration/src/main/resources/application.yaml b/metadata-service/configuration/src/main/resources/application.yaml index 9125bb046d7c8e..b88b5a3f1dbd0b 100644 --- a/metadata-service/configuration/src/main/resources/application.yaml +++ b/metadata-service/configuration/src/main/resources/application.yaml @@ -370,6 +370,7 @@ featureFlags: nestedDomainsEnabled: ${NESTED_DOMAINS_ENABLED:true} # Enables the nested Domains feature that allows users to have sub-Domains. If this is off, Domains appear "flat" again schemaFieldEntityFetchEnabled: ${SCHEMA_FIELD_ENTITY_FETCH_ENABLED:true} # Enables fetching for schema field entities from the database when we hydrate them on schema fields businessAttributeEntityEnabled: ${BUSINESS_ATTRIBUTE_ENTITY_ENABLED:false} # Enables business attribute entity which can be associated with field of dataset + dataContractsEnabled: ${DATA_CONTRACTS_ENABLED:true} # Enables the Data Contracts feature (Tab) in the UI entityChangeEvents: enabled: ${ENABLE_ENTITY_CHANGE_EVENTS_HOOK:true} From f74b7d9460d7155eacb03432f8a67874ff660d9d Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Fri, 14 Jun 2024 22:56:09 +0530 Subject: [PATCH 22/26] feat(ui/data-contracct): remove yarn lock file changes --- datahub-web-react/yarn.lock | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/datahub-web-react/yarn.lock b/datahub-web-react/yarn.lock index 702e499b2821ee..aad6d84c6bd93d 100644 --- a/datahub-web-react/yarn.lock +++ b/datahub-web-react/yarn.lock @@ -4645,10 +4645,15 @@ camelize@^1.0.0: resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= -caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001541: - version "1.0.30001625" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz" - integrity sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w== +caniuse-lite@^1.0.30001503: + version "1.0.30001508" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001508.tgz#4461bbc895c692a96da399639cc1e146e7302a33" + integrity sha512-sdQZOJdmt3GJs1UMNpCCCyeuS2IEGLXnHyAo9yIO5JJDjbjoVRij4M1qep6P6gFpptD1PqIYgzM+gwJbOi92mw== + +caniuse-lite@^1.0.30001541: + version "1.0.30001559" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001559.tgz#95a982440d3d314c471db68d02664fb7536c5a30" + integrity sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA== capital-case@^1.0.4: version "1.0.4" From b598719d33ffc48eac14b5a6d503c6bf4fe29fbd Mon Sep 17 00:00:00 2001 From: jayacryl <159848059+jayacryl@users.noreply.github.com> Date: Fri, 14 Jun 2024 13:26:30 -0400 Subject: [PATCH 23/26] nit fix --- datahub-web-react/src/appConfigContext.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/datahub-web-react/src/appConfigContext.tsx b/datahub-web-react/src/appConfigContext.tsx index b4f16e2d2a8240..4262f772b006ed 100644 --- a/datahub-web-react/src/appConfigContext.tsx +++ b/datahub-web-react/src/appConfigContext.tsx @@ -53,6 +53,7 @@ export const DEFAULT_APP_CONFIG = { nestedDomainsEnabled: true, platformBrowseV2: false, businessAttributeEntityEnabled: false, + dataContractsEnabled: false, }, }; From 26c99d7f9a7c1f7cefcd149302074424fd71df08 Mon Sep 17 00:00:00 2001 From: jayacryl <159848059+jayacryl@users.noreply.github.com> Date: Fri, 14 Jun 2024 13:27:53 -0400 Subject: [PATCH 24/26] disable data contracts by default --- .../configuration/src/main/resources/application.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metadata-service/configuration/src/main/resources/application.yaml b/metadata-service/configuration/src/main/resources/application.yaml index b88b5a3f1dbd0b..f2d5632b37041c 100644 --- a/metadata-service/configuration/src/main/resources/application.yaml +++ b/metadata-service/configuration/src/main/resources/application.yaml @@ -370,7 +370,7 @@ featureFlags: nestedDomainsEnabled: ${NESTED_DOMAINS_ENABLED:true} # Enables the nested Domains feature that allows users to have sub-Domains. If this is off, Domains appear "flat" again schemaFieldEntityFetchEnabled: ${SCHEMA_FIELD_ENTITY_FETCH_ENABLED:true} # Enables fetching for schema field entities from the database when we hydrate them on schema fields businessAttributeEntityEnabled: ${BUSINESS_ATTRIBUTE_ENTITY_ENABLED:false} # Enables business attribute entity which can be associated with field of dataset - dataContractsEnabled: ${DATA_CONTRACTS_ENABLED:true} # Enables the Data Contracts feature (Tab) in the UI + dataContractsEnabled: ${DATA_CONTRACTS_ENABLED:false} # Enables the Data Contracts feature (Tab) in the UI entityChangeEvents: enabled: ${ENABLE_ENTITY_CHANGE_EVENTS_HOOK:true} From 5dd312553a6bbcd4833478aa7113f0d32ad94ed8 Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Tue, 18 Jun 2024 22:26:46 +0530 Subject: [PATCH 25/26] feat(ui/data-contract): fix lint issues --- .../tabs/Dataset/Validations/Assertions.tsx | 2 +- .../Validations/DatasetAssertionsList.tsx | 3 +-- .../contract/DataContractEmptyState.tsx | 2 +- .../Validations/contract/DataContractTab.tsx | 4 +--- .../contract/builder/DataContractBuilder.tsx | 16 +++++----------- .../builder/DataContractBuilderModal.tsx | 15 ++------------- .../shared/tabs/Dataset/Validations/utils.tsx | 3 --- 7 files changed, 11 insertions(+), 34 deletions(-) diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx index 57c670e334e2b6..953433c6a8165f 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx @@ -53,7 +53,7 @@ export const Assertions = () => { const combinedData = isHideSiblingMode ? data : combineEntityDataWithSiblings(data); const [removedUrns, setRemovedUrns] = useState([]); - const { data: contractData, refetch: contractRefetch } = useGetDatasetContractQuery({ + const { data: contractData } = useGetDatasetContractQuery({ variables: { urn }, fetchPolicy: 'cache-first', }); diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx index 14edc712205d7f..79f337a8a13f89 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx @@ -9,6 +9,7 @@ import { StopOutlined, AuditOutlined, } from '@ant-design/icons'; +import { Link } from 'react-router-dom'; import { DatasetAssertionDescription } from './DatasetAssertionDescription'; import { StyledTable } from '../../../components/styled/StyledTable'; import { DatasetAssertionDetails } from './DatasetAssertionDetails'; @@ -17,11 +18,9 @@ import { getResultColor, getResultIcon, getResultText } from './assertionUtils'; import { useDeleteAssertionMutation } from '../../../../../../graphql/assertion.generated'; import { capitalizeFirstLetterOnly } from '../../../../../shared/textUtil'; import AssertionMenu from './AssertionMenu'; -import { Link, useParams } from 'react-router-dom'; import { REDESIGN_COLORS } from '../../../constants'; import { useEntityRegistry } from '../../../../../useEntityRegistry'; import { isAssertionPartOfContract } from './contract/utils'; -import { decodeUrn } from '../../../utils'; import { useEntityData } from '../../../EntityContext'; const ResultContainer = styled.div` diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState.tsx index 043d89b7cd7c2c..59a9d7a56821a7 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import styled from 'styled-components'; import { Button, Typography } from 'antd'; import { PlusOutlined } from '@ant-design/icons'; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx index 98cb9223fe8a9d..52a7eca8730be5 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx @@ -28,7 +28,7 @@ const RightColumn = styled.div` * Component used for rendering the Data Contract Tab on the Assertions parent tab. */ export const DataContractTab = () => { - const { urn, entityType } = useEntityData(); + const { urn } = useEntityData(); const { data, refetch } = useGetDatasetContractQuery({ variables: { @@ -109,9 +109,7 @@ export const DataContractTab = () => { initialState={createBuilderState(data?.dataset?.contract as any)} entityUrn={urn} onCancel={() => setShowContractBuilder(false)} - onPropose={onContractUpdate} onSubmit={onContractUpdate} - entityType={entityType} /> )} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx index e13ff6aab9bcbb..e3d85cb9db7cd1 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilder.tsx @@ -1,8 +1,8 @@ import React, { useState } from 'react'; -import { message, Button, Tooltip } from 'antd'; +import { message, Button } from 'antd'; import styled from 'styled-components'; import lodash from 'lodash'; -import { DataContract, AssertionType, EntityType, Assertion } from '../../../../../../../../types.generated'; +import { DataContract, AssertionType, Assertion } from '../../../../../../../../types.generated'; import { DataContractBuilderState, DataContractCategoryType, DEFAULT_BUILDER_STATE } from './types'; import { buildUpsertDataContractMutationVariables } from './utils'; import { useUpsertDataContractMutation } from '../../../../../../../../graphql/contract.generated'; @@ -32,10 +32,6 @@ const CancelButton = styled(Button)` margin-left: 12px; `; -const ProposeButton = styled(Button)` - margin-right: 12px; -`; - const SaveButton = styled(Button)` margin-right: 20px; `; @@ -44,9 +40,7 @@ type Props = { entityUrn: string; initialState?: DataContractBuilderState; onSubmit?: (contract: DataContract) => void; - onPropose?: () => void; onCancel?: () => void; - entityType?: EntityType; }; /** @@ -54,7 +48,7 @@ type Props = { * * In order to build a data contract, we simply list all dataset assertions and allow the user to choose. */ -export const DataContractBuilder = ({ entityUrn, entityType, initialState, onSubmit, onPropose, onCancel }: Props) => { +export const DataContractBuilder = ({ entityUrn, initialState, onSubmit, onCancel }: Props) => { const isEdit = !!initialState; const [builderState, setBuilderState] = useState(initialState || DEFAULT_BUILDER_STATE); const [upsertDataContractMutation] = useUpsertDataContractMutation(); @@ -81,13 +75,13 @@ export const DataContractBuilder = ({ entityUrn, entityType, initialState, onSub return upsertDataContractMutation({ variables: buildUpsertDataContractMutationVariables(entityUrn, builderState), }) - .then(({ data, errors }) => { + .then(({ data: dataContract, errors }) => { if (!errors) { message.success({ content: isEdit ? `Edited Data Contract` : `Created Data Contract!`, duration: 3, }); - onSubmit?.(data?.upsertDataContract as DataContract); + onSubmit?.(dataContract?.upsertDataContract as DataContract); } }) .catch(() => { diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilderModal.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilderModal.tsx index a788e8848c5841..75a8fe0410918b 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilderModal.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/contract/builder/DataContractBuilderModal.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Modal, Typography } from 'antd'; -import { DataContract, EntityType } from '../../../../../../../../types.generated'; +import { DataContract } from '../../../../../../../../types.generated'; import ClickOutside from '../../../../../../../shared/ClickOutside'; import { DataContractBuilderState } from './types'; import { DataContractBuilder } from './DataContractBuilder'; @@ -19,22 +19,13 @@ type Props = { entityUrn: string; initialState?: DataContractBuilderState; onSubmit?: (contract: DataContract) => void; - onPropose?: () => void; onCancel?: () => void; - entityType?: EntityType; }; /** * This component is a modal used for constructing new Data Contracts */ -export const DataContractBuilderModal = ({ - entityUrn, - initialState, - onSubmit, - onPropose, - onCancel, - entityType, -}: Props) => { +export const DataContractBuilderModal = ({ entityUrn, initialState, onSubmit, onCancel }: Props) => { const isEditing = initialState !== undefined; const titleText = isEditing ? 'Edit Data Contract' : 'New Data Contract'; @@ -66,10 +57,8 @@ export const DataContractBuilderModal = ({ > diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx index 5358ed07589bc4..e01f2ad2eae83e 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/utils.tsx @@ -1,7 +1,5 @@ import React from 'react'; import styled from 'styled-components'; -import * as moment from 'moment-timezone'; -import cronstrue from 'cronstrue'; import { ClockCircleOutlined, TableOutlined, @@ -26,7 +24,6 @@ import { } from '../../../../../../types.generated'; import { sortAssertions } from './assertionUtils'; import { AssertionGroup, AssertionStatusSummary } from './types'; -import { lowerFirstLetter } from '../../../../../shared/textUtil'; import { formatNumberWithoutAbbreviation } from '../../../../../shared/formatNumber'; import { parseMaybeStringAsFloatOrDefault } from '../../../../../shared/numberUtil'; From 650ac9527d9ee9f0249858cfa9e00840757cb790 Mon Sep 17 00:00:00 2001 From: Amit Gaikwad Date: Tue, 18 Jun 2024 23:30:50 +0530 Subject: [PATCH 26/26] feat(ui/data-contract): fix lint issues --- .../src/main/resources/entity.graphql | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index 316bdd7ef52790..964221c5a1e755 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -12581,3 +12581,18 @@ type ListBusinessAttributesResult { """ businessAttributes: [BusinessAttribute!]! } + +""" +A cron schedule +""" +type CronSchedule { + """ + A cron-formatted execution interval, as a cron string, e.g. 1 * * * * + """ + cron: String! + + """ + Timezone in which the cron interval applies, e.g. America/Los_Angeles + """ + timezone: String! +}