From 9cd0542d182f7bd0103802ae4804ab9fd1a209b0 Mon Sep 17 00:00:00 2001 From: poorvi767 Date: Wed, 20 Mar 2024 19:56:25 +0530 Subject: [PATCH] feat(models) : Joins (Datasets) schema, resolvers and UI (#8325) Co-authored-by: Raj Tekal Co-authored-by: Raj Tekal Co-authored-by: Raj Tekal Co-authored-by: Chris Collins --- .../datahub/graphql/GmsGraphQLEngine.java | 73 ++++ .../datahub/graphql/GmsGraphQLEngineArgs.java | 2 + .../graphql/featureflags/FeatureFlags.java | 1 + .../resolvers/config/AppConfigResolver.java | 6 + .../common/mappers/UrnToEntityMapper.java | 6 + .../types/entitytype/EntityTypeMapper.java | 1 + .../CreateERModelRelationshipResolver.java | 113 +++++ .../ERModelRelationshipType.java | 249 +++++++++++ .../UpdateERModelRelationshipResolver.java | 66 +++ .../mappers/ERModelRelationMapper.java | 185 ++++++++ .../ERModelRelationshipUpdateInputMapper.java | 190 ++++++++ .../src/main/resources/app.graphql | 4 + .../src/main/resources/entity.graphql | 246 +++++++++++ .../src/app/buildEntityRegistry.ts | 2 + .../src/app/entity/dataset/DatasetEntity.tsx | 9 + .../ERModelRelationshipEntity.tsx | 141 ++++++ .../preview/ERModelRelationshipAction.less | 12 + .../ERModelRelationshipPreviewCard.tsx | 54 +++ .../CreateERModelRelationModal.less | 294 +++++++++++++ .../CreateERModelRelationModal.tsx | 409 ++++++++++++++++++ .../ERModelRelationPreview.less | 176 ++++++++ .../ERModelRelationPreview.tsx | 191 ++++++++ .../ERModelRelationUtils.tsx | 63 +++ .../ERModelRelationship/EditableCell.tsx | 75 ++++ .../ERModelRelationship/EditableRow.tsx | 17 + .../search/EmbeddedListSearchResults.tsx | 3 + .../styled/search/EntitySearchResults.tsx | 44 +- .../components/styled/search/SearchSelect.tsx | 34 +- .../styled/search/SearchSelectModal.tsx | 6 + .../containers/profile/EntityProfile.tsx | 15 + .../tabs/Dataset/Queries/QueryBuilderForm.tsx | 2 +- .../Relationship/RelationshipsTab.less | 84 ++++ .../Dataset/Relationship/RelationshipsTab.tsx | 273 ++++++++++++ .../ERModelRelationshipTab.less | 44 ++ .../ERModelRelationshipTab.tsx | 21 + datahub-web-react/src/appConfigContext.tsx | 1 + datahub-web-react/src/graphql/app.graphql | 1 + .../src/graphql/ermodelrelationship.graphql | 57 +++ .../src/graphql/fragments.graphql | 41 ++ datahub-web-react/src/graphql/lineage.graphql | 20 + .../src/graphql/relationships.graphql | 2 +- datahub-web-react/src/graphql/search.graphql | 20 + datahub-web-react/src/images/Arrow.svg | 3 + datahub-web-react/src/images/close_dark.svg | 3 + .../src/images/editIconBlack.svg | 3 + .../src/images/ermodelrelationshipIcon.svg | 3 + docker/docker-compose.dev.yml | 5 +- docs/deploy/environment-vars.md | 6 +- .../java/com/linkedin/metadata/Constants.java | 8 + .../common/urn/ERModelRelationshipUrn.java | 71 +++ .../common/ERModelRelationshipUrn.pdl | 24 + .../ERModelRelationshipProperties.pdl | 90 ++++ .../EditableERModelRelationshipProperties.pdl | 31 ++ .../RelationshipFieldMapping.pdl | 18 + .../metadata/key/ERModelRelationshipKey.pdl | 14 + .../src/main/resources/entity-registry.yml | 11 + .../src/main/resources/application.yml | 1 + .../ERModelRelationshipServiceFactory.java | 30 ++ .../factory/graphql/GraphQLEngineFactory.java | 6 + .../service/ERModelRelationshipService.java | 68 +++ .../war/src/main/resources/boot/policies.json | 1 + .../authorization/PoliciesConfig.java | 19 +- 62 files changed, 3638 insertions(+), 30 deletions(-) create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/CreateERModelRelationshipResolver.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/ERModelRelationshipType.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/UpdateERModelRelationshipResolver.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/mappers/ERModelRelationMapper.java create mode 100644 datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/mappers/ERModelRelationshipUpdateInputMapper.java create mode 100644 datahub-web-react/src/app/entity/ermodelrelationships/ERModelRelationshipEntity.tsx create mode 100644 datahub-web-react/src/app/entity/ermodelrelationships/preview/ERModelRelationshipAction.less create mode 100644 datahub-web-react/src/app/entity/ermodelrelationships/preview/ERModelRelationshipPreviewCard.tsx create mode 100644 datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/CreateERModelRelationModal.less create mode 100644 datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/CreateERModelRelationModal.tsx create mode 100644 datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/ERModelRelationPreview.less create mode 100644 datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/ERModelRelationPreview.tsx create mode 100644 datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/ERModelRelationUtils.tsx create mode 100644 datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/EditableCell.tsx create mode 100644 datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/EditableRow.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Relationship/RelationshipsTab.less create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Relationship/RelationshipsTab.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/ERModelRelationship/ERModelRelationshipTab.less create mode 100644 datahub-web-react/src/app/entity/shared/tabs/ERModelRelationship/ERModelRelationshipTab.tsx create mode 100644 datahub-web-react/src/graphql/ermodelrelationship.graphql create mode 100644 datahub-web-react/src/images/Arrow.svg create mode 100644 datahub-web-react/src/images/close_dark.svg create mode 100644 datahub-web-react/src/images/editIconBlack.svg create mode 100644 datahub-web-react/src/images/ermodelrelationshipIcon.svg create mode 100644 li-utils/src/main/javaPegasus/com/linkedin/common/urn/ERModelRelationshipUrn.java create mode 100644 li-utils/src/main/pegasus/com/linkedin/common/ERModelRelationshipUrn.pdl create mode 100644 metadata-models/src/main/pegasus/com/linkedin/ermodelrelation/ERModelRelationshipProperties.pdl create mode 100644 metadata-models/src/main/pegasus/com/linkedin/ermodelrelation/EditableERModelRelationshipProperties.pdl create mode 100644 metadata-models/src/main/pegasus/com/linkedin/ermodelrelation/RelationshipFieldMapping.pdl create mode 100644 metadata-models/src/main/pegasus/com/linkedin/metadata/key/ERModelRelationshipKey.pdl create mode 100644 metadata-service/factories/src/main/java/com/linkedin/gms/factory/ermodelrelation/ERModelRelationshipServiceFactory.java create mode 100644 metadata-service/services/src/main/java/com/linkedin/metadata/service/ERModelRelationshipService.java 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 e9d94d313b70ec..7bf892ff2b5d5c 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 @@ -53,6 +53,7 @@ import com.linkedin.datahub.graphql.generated.Dataset; import com.linkedin.datahub.graphql.generated.DatasetStatsSummary; import com.linkedin.datahub.graphql.generated.Domain; +import com.linkedin.datahub.graphql.generated.ERModelRelationshipProperties; import com.linkedin.datahub.graphql.generated.EntityPath; import com.linkedin.datahub.graphql.generated.EntityRelationship; import com.linkedin.datahub.graphql.generated.EntityRelationshipLegacy; @@ -308,6 +309,9 @@ import com.linkedin.datahub.graphql.types.datatype.DataTypeType; import com.linkedin.datahub.graphql.types.domain.DomainType; import com.linkedin.datahub.graphql.types.entitytype.EntityTypeType; +import com.linkedin.datahub.graphql.types.ermodelrelationship.CreateERModelRelationshipResolver; +import com.linkedin.datahub.graphql.types.ermodelrelationship.ERModelRelationshipType; +import com.linkedin.datahub.graphql.types.ermodelrelationship.UpdateERModelRelationshipResolver; import com.linkedin.datahub.graphql.types.form.FormType; import com.linkedin.datahub.graphql.types.glossary.GlossaryNodeType; import com.linkedin.datahub.graphql.types.glossary.GlossaryTermType; @@ -346,6 +350,7 @@ import com.linkedin.metadata.recommendation.RecommendationsService; import com.linkedin.metadata.secret.SecretService; import com.linkedin.metadata.service.DataProductService; +import com.linkedin.metadata.service.ERModelRelationshipService; import com.linkedin.metadata.service.FormService; import com.linkedin.metadata.service.LineageService; import com.linkedin.metadata.service.OwnershipTypeService; @@ -417,6 +422,7 @@ public class GmsGraphQLEngine { private final LineageService lineageService; private final QueryService queryService; private final DataProductService dataProductService; + private final ERModelRelationshipService erModelRelationshipService; private final FormService formService; private final RestrictedService restrictedService; @@ -462,6 +468,7 @@ public class GmsGraphQLEngine { private final DataHubPolicyType dataHubPolicyType; private final DataHubRoleType dataHubRoleType; private final SchemaFieldType schemaFieldType; + private final ERModelRelationshipType erModelRelationshipType; private final DataHubViewType dataHubViewType; private final QueryType queryType; private final DataProductType dataProductType; @@ -529,6 +536,7 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) { this.settingsService = args.settingsService; this.lineageService = args.lineageService; this.queryService = args.queryService; + this.erModelRelationshipService = args.erModelRelationshipService; this.dataProductService = args.dataProductService; this.formService = args.formService; this.restrictedService = args.restrictedService; @@ -572,6 +580,7 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) { this.dataHubPolicyType = new DataHubPolicyType(entityClient); this.dataHubRoleType = new DataHubRoleType(entityClient); this.schemaFieldType = new SchemaFieldType(entityClient, featureFlags); + this.erModelRelationshipType = new ERModelRelationshipType(entityClient, featureFlags); this.dataHubViewType = new DataHubViewType(entityClient); this.queryType = new QueryType(entityClient); this.dataProductType = new DataProductType(entityClient); @@ -617,6 +626,7 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) { dataHubPolicyType, dataHubRoleType, schemaFieldType, + erModelRelationshipType, dataHubViewType, queryType, dataProductType, @@ -707,6 +717,7 @@ public void configureRuntimeWiring(final RuntimeWiring.Builder builder) { configureTestResultResolvers(builder); configureRoleResolvers(builder); configureSchemaFieldResolvers(builder); + configureERModelRelationshipResolvers(builder); configureEntityPathResolvers(builder); configureResolvedAuditStampResolvers(builder); configureViewResolvers(builder); @@ -939,6 +950,7 @@ private void configureQueryResolvers(final RuntimeWiring.Builder builder) { .dataFetcher("glossaryTerm", getResolver(glossaryTermType)) .dataFetcher("glossaryNode", getResolver(glossaryNodeType)) .dataFetcher("domain", getResolver((domainType))) + .dataFetcher("erModelRelationship", getResolver(erModelRelationshipType)) .dataFetcher("dataPlatform", getResolver(dataPlatformType)) .dataFetcher("dataPlatformInstance", getResolver(dataPlatformInstanceType)) .dataFetcher("mlFeatureTable", getResolver(mlFeatureTableType)) @@ -1069,6 +1081,13 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) { .dataFetcher("updateDataFlow", new MutableTypeResolver<>(dataFlowType)) .dataFetcher("updateCorpUserProperties", new MutableTypeResolver<>(corpUserType)) .dataFetcher("updateCorpGroupProperties", new MutableTypeResolver<>(corpGroupType)) + .dataFetcher( + "updateERModelRelationship", + new UpdateERModelRelationshipResolver(this.entityClient)) + .dataFetcher( + "createERModelRelationship", + new CreateERModelRelationshipResolver( + this.entityClient, this.erModelRelationshipService)) .dataFetcher("addTag", new AddTagResolver(entityService)) .dataFetcher("addTags", new AddTagsResolver(entityService)) .dataFetcher("batchAddTags", new BatchAddTagsResolver(entityService)) @@ -2078,6 +2097,60 @@ private void configureTypeExtensions(final RuntimeWiring.Builder builder) { builder.scalar(GraphQLLong); } + /** Configures resolvers responsible for resolving the {@link ERModelRelationship} type. */ + private void configureERModelRelationshipResolvers(final RuntimeWiring.Builder builder) { + builder + .type( + "ERModelRelationship", + typeWiring -> + typeWiring + .dataFetcher("privileges", new EntityPrivilegesResolver(entityClient)) + .dataFetcher( + "relationships", new EntityRelationshipsResultResolver(graphClient))) + .type( + "ERModelRelationshipProperties", + typeWiring -> + typeWiring + .dataFetcher( + "source", + new LoadableTypeResolver<>( + datasetType, + (env) -> { + final ERModelRelationshipProperties erModelRelationshipProperties = + env.getSource(); + return erModelRelationshipProperties.getSource() != null + ? erModelRelationshipProperties.getSource().getUrn() + : null; + })) + .dataFetcher( + "destination", + new LoadableTypeResolver<>( + datasetType, + (env) -> { + final ERModelRelationshipProperties erModelRelationshipProperties = + env.getSource(); + return erModelRelationshipProperties.getDestination() != null + ? erModelRelationshipProperties.getDestination().getUrn() + : null; + }))) + .type( + "Owner", + typeWiring -> + typeWiring.dataFetcher( + "owner", + new OwnerTypeResolver<>( + ownerTypes, (env) -> ((Owner) env.getSource()).getOwner()))) + .type( + "InstitutionalMemoryMetadata", + typeWiring -> + typeWiring.dataFetcher( + "author", + new LoadableTypeResolver<>( + corpUserType, + (env) -> + ((InstitutionalMemoryMetadata) env.getSource()).getAuthor().getUrn()))); + } + /** * Configures resolvers responsible for resolving the {@link * com.linkedin.datahub.graphql.generated.DataJob} type. diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngineArgs.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngineArgs.java index df32530129b040..db63dfc19b398f 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngineArgs.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngineArgs.java @@ -25,6 +25,7 @@ import com.linkedin.metadata.recommendation.RecommendationsService; import com.linkedin.metadata.secret.SecretService; import com.linkedin.metadata.service.DataProductService; +import com.linkedin.metadata.service.ERModelRelationshipService; import com.linkedin.metadata.service.FormService; import com.linkedin.metadata.service.LineageService; import com.linkedin.metadata.service.OwnershipTypeService; @@ -75,6 +76,7 @@ public class GmsGraphQLEngineArgs { QueryService queryService; FeatureFlags featureFlags; DataProductService dataProductService; + ERModelRelationshipService erModelRelationshipService; FormService formService; RestrictedService restrictedService; int graphQLQueryComplexityLimit; 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 667ccd368a7291..8bc716f4ff4db5 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 @@ -15,6 +15,7 @@ public class FeatureFlags { private boolean platformBrowseV2 = false; private PreProcessHooks preProcessHooks; private boolean showAcrylInfo = false; + private boolean erModelRelationshipFeatureEnabled = false; private boolean showAccessManagement = false; private boolean nestedDomainsEnabled = false; private boolean schemaFieldEntityFetchEnabled = 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 f127e6a49abfff..d884afb36a280a 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 @@ -179,6 +179,8 @@ public CompletableFuture get(final DataFetchingEnvironment environmen .setReadOnlyModeEnabled(_featureFlags.isReadOnlyModeEnabled()) .setShowBrowseV2(_featureFlags.isShowBrowseV2()) .setShowAcrylInfo(_featureFlags.isShowAcrylInfo()) + .setErModelRelationshipFeatureEnabled( + _featureFlags.isErModelRelationshipFeatureEnabled()) .setShowAccessManagement(_featureFlags.isShowAccessManagement()) .setNestedDomainsEnabled(_featureFlags.isNestedDomainsEnabled()) .setPlatformBrowseV2(_featureFlags.isPlatformBrowseV2()) @@ -262,6 +264,10 @@ private EntityType mapResourceTypeToEntityType(final String resourceType) { .getResourceType() .equals(resourceType)) { return EntityType.CORP_USER; + } else if (com.linkedin.metadata.authorization.PoliciesConfig.ER_MODEL_RELATIONSHIP_PRIVILEGES + .getResourceType() + .equals(resourceType)) { + return EntityType.ER_MODEL_RELATIONSHIP; } else { return null; } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/UrnToEntityMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/UrnToEntityMapper.java index a859cd6c79e80d..18050b11937552 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/UrnToEntityMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/UrnToEntityMapper.java @@ -19,6 +19,7 @@ import com.linkedin.datahub.graphql.generated.DataProduct; import com.linkedin.datahub.graphql.generated.Dataset; import com.linkedin.datahub.graphql.generated.Domain; +import com.linkedin.datahub.graphql.generated.ERModelRelationship; import com.linkedin.datahub.graphql.generated.Entity; import com.linkedin.datahub.graphql.generated.EntityType; import com.linkedin.datahub.graphql.generated.GlossaryNode; @@ -155,6 +156,11 @@ public Entity apply(Urn input) { ((Domain) partialEntity).setUrn(input.toString()); ((Domain) partialEntity).setType(EntityType.DOMAIN); } + if (input.getEntityType().equals("erModelRelationship")) { + partialEntity = new ERModelRelationship(); + ((ERModelRelationship) partialEntity).setUrn(input.toString()); + ((ERModelRelationship) partialEntity).setType(EntityType.ER_MODEL_RELATIONSHIP); + } if (input.getEntityType().equals("assertion")) { partialEntity = new Assertion(); ((Assertion) partialEntity).setUrn(input.toString()); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/entitytype/EntityTypeMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/entitytype/EntityTypeMapper.java index 3ecd01e99056b4..e36d4e17f564da 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/entitytype/EntityTypeMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/entitytype/EntityTypeMapper.java @@ -38,6 +38,7 @@ public class EntityTypeMapper { .put(EntityType.NOTEBOOK, "notebook") .put(EntityType.DATA_PLATFORM_INSTANCE, "dataPlatformInstance") .put(EntityType.TEST, "test") + .put(EntityType.ER_MODEL_RELATIONSHIP, Constants.ER_MODEL_RELATIONSHIP_ENTITY_NAME) .put(EntityType.DATAHUB_VIEW, Constants.DATAHUB_VIEW_ENTITY_NAME) .put(EntityType.DATA_PRODUCT, Constants.DATA_PRODUCT_ENTITY_NAME) .put(EntityType.SCHEMA_FIELD, "schemaField") diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/CreateERModelRelationshipResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/CreateERModelRelationshipResolver.java new file mode 100644 index 00000000000000..8d657a33ff6510 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/CreateERModelRelationshipResolver.java @@ -0,0 +1,113 @@ +package com.linkedin.datahub.graphql.types.ermodelrelationship; + +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; + +import com.datahub.authentication.Authentication; +import com.linkedin.common.urn.CorpuserUrn; +import com.linkedin.common.urn.ERModelRelationshipUrn; +import com.linkedin.common.urn.Urn; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.exception.AuthorizationException; +import com.linkedin.datahub.graphql.generated.ERModelRelationship; +import com.linkedin.datahub.graphql.generated.ERModelRelationshipPropertiesInput; +import com.linkedin.datahub.graphql.generated.ERModelRelationshipUpdateInput; +import com.linkedin.datahub.graphql.types.ermodelrelationship.mappers.ERModelRelationMapper; +import com.linkedin.datahub.graphql.types.ermodelrelationship.mappers.ERModelRelationshipUpdateInputMapper; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.service.ERModelRelationshipService; +import com.linkedin.mxe.MetadataChangeProposal; +import com.linkedin.r2.RemoteInvocationException; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.digest.DigestUtils; + +@Slf4j +@RequiredArgsConstructor +public class CreateERModelRelationshipResolver + implements DataFetcher> { + + private final EntityClient _entityClient; + private final ERModelRelationshipService _erModelRelationshipService; + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) + throws Exception { + final ERModelRelationshipUpdateInput input = + bindArgument(environment.getArgument("input"), ERModelRelationshipUpdateInput.class); + + final ERModelRelationshipPropertiesInput erModelRelationshipPropertiesInput = + input.getProperties(); + String ermodelrelationName = erModelRelationshipPropertiesInput.getName(); + String source = erModelRelationshipPropertiesInput.getSource(); + String destination = erModelRelationshipPropertiesInput.getDestination(); + + String lowDataset = source; + String highDataset = destination; + if (source.compareTo(destination) > 0) { + lowDataset = destination; + highDataset = source; + } + // The following sequence mimics datahub.emitter.mce_builder.datahub_guid + + String ermodelrelationKey = + "{\"Source\":\"" + + lowDataset + + "\",\"Destination\":\"" + + highDataset + + "\",\"ERModelRelationName\":\"" + + ermodelrelationName + + "\"}"; + + byte[] mybytes = ermodelrelationKey.getBytes(StandardCharsets.UTF_8); + + String ermodelrelationKeyEncoded = new String(mybytes, StandardCharsets.UTF_8); + String ermodelrelationGuid = DigestUtils.md5Hex(ermodelrelationKeyEncoded); + log.info( + "ermodelrelationkey {}, ermodelrelationGuid {}", + ermodelrelationKeyEncoded, + ermodelrelationGuid); + + ERModelRelationshipUrn inputUrn = new ERModelRelationshipUrn(ermodelrelationGuid); + QueryContext context = environment.getContext(); + final Authentication authentication = context.getAuthentication(); + final CorpuserUrn actor = CorpuserUrn.createFromString(context.getActorUrn()); + if (!ERModelRelationshipType.canCreateERModelRelation( + context, + Urn.createFromString(input.getProperties().getSource()), + Urn.createFromString(input.getProperties().getDestination()))) { + throw new AuthorizationException( + "Unauthorized to create erModelRelationship. Please contact your DataHub administrator."); + } + return CompletableFuture.supplyAsync( + () -> { + try { + log.debug("Create ERModelRelation input: {}", input); + final Collection proposals = + ERModelRelationshipUpdateInputMapper.map(input, actor); + proposals.forEach(proposal -> proposal.setEntityUrn(inputUrn)); + try { + _entityClient.batchIngestProposals(proposals, context.getAuthentication(), false); + } catch (RemoteInvocationException e) { + throw new RuntimeException("Failed to create erModelRelationship entity", e); + } + return ERModelRelationMapper.map( + _erModelRelationshipService.getERModelRelationshipResponse( + Urn.createFromString(inputUrn.toString()), authentication)); + } catch (Exception e) { + log.error( + "Failed to create ERModelRelation to resource with input {}, {}", + input, + e.getMessage()); + throw new RuntimeException( + String.format( + "Failed to create erModelRelationship to resource with input %s", input), + e); + } + }); + } +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/ERModelRelationshipType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/ERModelRelationshipType.java new file mode 100644 index 00000000000000..684680597f54d4 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/ERModelRelationshipType.java @@ -0,0 +1,249 @@ +package com.linkedin.datahub.graphql.types.ermodelrelationship; + +import static com.linkedin.datahub.graphql.Constants.*; +import static com.linkedin.metadata.Constants.*; + +import com.datahub.authorization.ConjunctivePrivilegeGroup; +import com.datahub.authorization.DisjunctivePrivilegeGroup; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.linkedin.common.urn.ERModelRelationshipUrn; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.data.template.StringArray; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; +import com.linkedin.datahub.graphql.featureflags.FeatureFlags; +import com.linkedin.datahub.graphql.generated.AutoCompleteResults; +import com.linkedin.datahub.graphql.generated.BrowsePath; +import com.linkedin.datahub.graphql.generated.BrowseResults; +import com.linkedin.datahub.graphql.generated.ERModelRelationship; +import com.linkedin.datahub.graphql.generated.ERModelRelationshipUpdateInput; +import com.linkedin.datahub.graphql.generated.Entity; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.generated.FacetFilterInput; +import com.linkedin.datahub.graphql.generated.SearchResults; +import com.linkedin.datahub.graphql.resolvers.ResolverUtils; +import com.linkedin.datahub.graphql.types.BrowsableEntityType; +import com.linkedin.datahub.graphql.types.SearchableEntityType; +import com.linkedin.datahub.graphql.types.ermodelrelationship.mappers.ERModelRelationMapper; +import com.linkedin.datahub.graphql.types.mappers.AutoCompleteResultsMapper; +import com.linkedin.datahub.graphql.types.mappers.BrowsePathsMapper; +import com.linkedin.datahub.graphql.types.mappers.BrowseResultMapper; +import com.linkedin.datahub.graphql.types.mappers.UrnSearchResultsMapper; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.authorization.PoliciesConfig; +import com.linkedin.metadata.browse.BrowseResult; +import com.linkedin.metadata.query.AutoCompleteResult; +import com.linkedin.metadata.query.filter.Filter; +import com.linkedin.metadata.search.SearchResult; +import graphql.execution.DataFetcherResult; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class ERModelRelationshipType + implements com.linkedin.datahub.graphql.types.EntityType, + BrowsableEntityType, + SearchableEntityType { + + static final Set ASPECTS_TO_RESOLVE = + ImmutableSet.of( + ER_MODEL_RELATIONSHIP_KEY_ASPECT_NAME, + ER_MODEL_RELATIONSHIP_PROPERTIES_ASPECT_NAME, + EDITABLE_ER_MODEL_RELATIONSHIP_PROPERTIES_ASPECT_NAME, + INSTITUTIONAL_MEMORY_ASPECT_NAME, + OWNERSHIP_ASPECT_NAME, + STATUS_ASPECT_NAME, + GLOBAL_TAGS_ASPECT_NAME, + GLOSSARY_TERMS_ASPECT_NAME); + + private static final Set FACET_FIELDS = ImmutableSet.of("name"); + private static final String ENTITY_NAME = "erModelRelationship"; + + private final EntityClient _entityClient; + private final FeatureFlags _featureFlags; + + public ERModelRelationshipType(final EntityClient entityClient, final FeatureFlags featureFlags) { + _entityClient = entityClient; + _featureFlags = + featureFlags; // TODO: check if ERModelRelation Feture is Enabled and throw error when + // called + } + + @Override + public Class objectClass() { + return ERModelRelationship.class; + } + + @Override + public EntityType type() { + return EntityType.ER_MODEL_RELATIONSHIP; + } + + @Override + public Function getKeyProvider() { + return Entity::getUrn; + } + + @Override + public List> batchLoad( + @Nonnull final List urns, @Nonnull final QueryContext context) throws Exception { + final List ermodelrelationUrns = + urns.stream().map(UrnUtils::getUrn).collect(Collectors.toList()); + + try { + final Map entities = + _entityClient.batchGetV2( + ER_MODEL_RELATIONSHIP_ENTITY_NAME, + new HashSet<>(ermodelrelationUrns), + ASPECTS_TO_RESOLVE, + context.getAuthentication()); + + final List gmsResults = new ArrayList<>(); + for (Urn urn : ermodelrelationUrns) { + gmsResults.add(entities.getOrDefault(urn, null)); + } + return gmsResults.stream() + .map( + gmsResult -> + gmsResult == null + ? null + : DataFetcherResult.newResult() + .data(ERModelRelationMapper.map(gmsResult)) + .build()) + .collect(Collectors.toList()); + } catch (Exception e) { + throw new RuntimeException("Failed to load erModelRelationship entity", e); + } + } + + @Nonnull + @Override + public BrowseResults browse( + @Nonnull List path, + @Nullable List filters, + int start, + int count, + @Nonnull QueryContext context) + throws Exception { + final Map facetFilters = ResolverUtils.buildFacetFilters(filters, FACET_FIELDS); + final String pathStr = + path.size() > 0 ? BROWSE_PATH_DELIMITER + String.join(BROWSE_PATH_DELIMITER, path) : ""; + final BrowseResult result = + _entityClient.browse( + context.getOperationContext().withSearchFlags(flags -> flags.setFulltext(false)), + "erModelRelationship", + pathStr, + facetFilters, + start, + count); + return BrowseResultMapper.map(result); + } + + @Nonnull + @Override + public List browsePaths(@Nonnull String urn, @Nonnull QueryContext context) + throws Exception { + final StringArray result = + _entityClient.getBrowsePaths(UrnUtils.getUrn(urn), context.getAuthentication()); + return BrowsePathsMapper.map(result); + } + + @Override + public SearchResults search( + @Nonnull String query, + @Nullable List filters, + int start, + int count, + @Nonnull QueryContext context) + throws Exception { + final Map facetFilters = ResolverUtils.buildFacetFilters(filters, FACET_FIELDS); + final SearchResult searchResult = + _entityClient.search( + context.getOperationContext().withSearchFlags(flags -> flags.setFulltext(true)), + ENTITY_NAME, + query, + facetFilters, + start, + count); + return UrnSearchResultsMapper.map(searchResult); + } + + @Override + public AutoCompleteResults autoComplete( + @Nonnull String query, + @Nullable String field, + @Nullable Filter filters, + int limit, + @Nonnull QueryContext context) + throws Exception { + final AutoCompleteResult result = + _entityClient.autoComplete( + context.getOperationContext(), ENTITY_NAME, query, filters, limit); + return AutoCompleteResultsMapper.map(result); + } + + public static boolean canUpdateERModelRelation( + @Nonnull QueryContext context, + ERModelRelationshipUrn resourceUrn, + ERModelRelationshipUpdateInput updateInput) { + final ConjunctivePrivilegeGroup editPrivilegesGroup = + new ConjunctivePrivilegeGroup( + ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType())); + List specificPrivileges = new ArrayList<>(); + if (updateInput.getEditableProperties() != null) { + specificPrivileges.add(PoliciesConfig.EDIT_ENTITY_DOCS_PRIVILEGE.getType()); + } + final ConjunctivePrivilegeGroup specificPrivilegeGroup = + new ConjunctivePrivilegeGroup(specificPrivileges); + + // If you either have all entity privileges, or have the specific privileges required, you are + // authorized. + DisjunctivePrivilegeGroup orPrivilegeGroups = + new DisjunctivePrivilegeGroup( + ImmutableList.of(editPrivilegesGroup, specificPrivilegeGroup)); + return AuthorizationUtils.isAuthorized( + context.getAuthorizer(), + context.getActorUrn(), + resourceUrn.getEntityType(), + resourceUrn.toString(), + orPrivilegeGroups); + } + + public static boolean canCreateERModelRelation( + @Nonnull QueryContext context, Urn sourceUrn, Urn destinationUrn) { + final ConjunctivePrivilegeGroup editPrivilegesGroup = + new ConjunctivePrivilegeGroup( + ImmutableList.of(PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType())); + final ConjunctivePrivilegeGroup createPrivilegesGroup = + new ConjunctivePrivilegeGroup( + ImmutableList.of(PoliciesConfig.CREATE_ER_MODEL_RELATIONSHIP_PRIVILEGE.getType())); + // If you either have all entity privileges, or have the specific privileges required, you are + // authorized. + DisjunctivePrivilegeGroup orPrivilegeGroups = + new DisjunctivePrivilegeGroup(ImmutableList.of(editPrivilegesGroup, createPrivilegesGroup)); + boolean sourcePrivilege = + AuthorizationUtils.isAuthorized( + context.getAuthorizer(), + context.getActorUrn(), + sourceUrn.getEntityType(), + sourceUrn.toString(), + orPrivilegeGroups); + boolean destinationPrivilege = + AuthorizationUtils.isAuthorized( + context.getAuthorizer(), + context.getActorUrn(), + destinationUrn.getEntityType(), + destinationUrn.toString(), + orPrivilegeGroups); + return sourcePrivilege && destinationPrivilege; + } +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/UpdateERModelRelationshipResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/UpdateERModelRelationshipResolver.java new file mode 100644 index 00000000000000..14d3a14fd6c426 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/UpdateERModelRelationshipResolver.java @@ -0,0 +1,66 @@ +package com.linkedin.datahub.graphql.types.ermodelrelationship; + +import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; + +import com.linkedin.common.urn.CorpuserUrn; +import com.linkedin.common.urn.ERModelRelationshipUrn; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.exception.AuthorizationException; +import com.linkedin.datahub.graphql.generated.ERModelRelationshipUpdateInput; +import com.linkedin.datahub.graphql.types.ermodelrelationship.mappers.ERModelRelationshipUpdateInputMapper; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.mxe.MetadataChangeProposal; +import com.linkedin.r2.RemoteInvocationException; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class UpdateERModelRelationshipResolver implements DataFetcher> { + + private final EntityClient _entityClient; + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + final ERModelRelationshipUpdateInput input = + bindArgument(environment.getArgument("input"), ERModelRelationshipUpdateInput.class); + final String urn = bindArgument(environment.getArgument("urn"), String.class); + ERModelRelationshipUrn inputUrn = ERModelRelationshipUrn.createFromString(urn); + QueryContext context = environment.getContext(); + final CorpuserUrn actor = CorpuserUrn.createFromString(context.getActorUrn()); + if (!ERModelRelationshipType.canUpdateERModelRelation(context, inputUrn, input)) { + throw new AuthorizationException( + "Unauthorized to perform this action. Please contact your DataHub administrator."); + } + return CompletableFuture.supplyAsync( + () -> { + try { + log.debug("Create ERModelRelation input: {}", input); + final Collection proposals = + ERModelRelationshipUpdateInputMapper.map(input, actor); + proposals.forEach(proposal -> proposal.setEntityUrn(inputUrn)); + + try { + _entityClient.batchIngestProposals(proposals, context.getAuthentication(), false); + } catch (RemoteInvocationException e) { + throw new RuntimeException( + String.format("Failed to update erModelRelationship entity"), e); + } + return true; + } catch (Exception e) { + log.error( + "Failed to update erModelRelationship to resource with input {}, {}", + input, + e.getMessage()); + throw new RuntimeException( + String.format( + "Failed to update erModelRelationship to resource with input %s", input), + e); + } + }); + } +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/mappers/ERModelRelationMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/mappers/ERModelRelationMapper.java new file mode 100644 index 00000000000000..f8649cadca9c4c --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/mappers/ERModelRelationMapper.java @@ -0,0 +1,185 @@ +package com.linkedin.datahub.graphql.types.ermodelrelationship.mappers; + +import static com.linkedin.metadata.Constants.*; + +import com.linkedin.common.GlobalTags; +import com.linkedin.common.GlossaryTerms; +import com.linkedin.common.InstitutionalMemory; +import com.linkedin.common.Ownership; +import com.linkedin.common.Status; +import com.linkedin.common.urn.Urn; +import com.linkedin.data.DataMap; +import com.linkedin.data.template.RecordTemplate; +import com.linkedin.datahub.graphql.generated.Dataset; +import com.linkedin.datahub.graphql.generated.ERModelRelationship; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.generated.RelationshipFieldMapping; +import com.linkedin.datahub.graphql.types.common.mappers.InstitutionalMemoryMapper; +import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper; +import com.linkedin.datahub.graphql.types.common.mappers.StatusMapper; +import com.linkedin.datahub.graphql.types.common.mappers.UrnToEntityMapper; +import com.linkedin.datahub.graphql.types.common.mappers.util.MappingHelper; +import com.linkedin.datahub.graphql.types.glossary.mappers.GlossaryTermsMapper; +import com.linkedin.datahub.graphql.types.mappers.ModelMapper; +import com.linkedin.datahub.graphql.types.tag.mappers.GlobalTagsMapper; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.EnvelopedAspectMap; +import com.linkedin.ermodelrelation.ERModelRelationshipProperties; +import com.linkedin.ermodelrelation.EditableERModelRelationshipProperties; +import com.linkedin.metadata.key.ERModelRelationshipKey; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * Maps Pegasus {@link RecordTemplate} objects to objects conforming to the GQL schema. + * + *

To be replaced by auto-generated mappers implementations + */ +public class ERModelRelationMapper implements ModelMapper { + + public static final ERModelRelationMapper INSTANCE = new ERModelRelationMapper(); + + public static ERModelRelationship map(@Nonnull final EntityResponse entityResponse) { + return INSTANCE.apply(entityResponse); + } + + @Override + public ERModelRelationship apply(final EntityResponse entityResponse) { + final ERModelRelationship result = new ERModelRelationship(); + final Urn entityUrn = entityResponse.getUrn(); + + result.setUrn(entityUrn.toString()); + result.setType(EntityType.ER_MODEL_RELATIONSHIP); + + final EnvelopedAspectMap aspectMap = entityResponse.getAspects(); + MappingHelper mappingHelper = new MappingHelper<>(aspectMap, result); + mappingHelper.mapToResult(ER_MODEL_RELATIONSHIP_KEY_ASPECT_NAME, this::mapERModelRelationKey); + mappingHelper.mapToResult(ER_MODEL_RELATIONSHIP_PROPERTIES_ASPECT_NAME, this::mapProperties); + if (aspectMap != null + && aspectMap.containsKey(EDITABLE_ER_MODEL_RELATIONSHIP_PROPERTIES_ASPECT_NAME)) { + mappingHelper.mapToResult( + EDITABLE_ER_MODEL_RELATIONSHIP_PROPERTIES_ASPECT_NAME, this::mapEditableProperties); + } + if (aspectMap != null && aspectMap.containsKey(INSTITUTIONAL_MEMORY_ASPECT_NAME)) { + mappingHelper.mapToResult( + INSTITUTIONAL_MEMORY_ASPECT_NAME, + (ermodelrelation, dataMap) -> + ermodelrelation.setInstitutionalMemory( + InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap), entityUrn))); + } + if (aspectMap != null && aspectMap.containsKey(OWNERSHIP_ASPECT_NAME)) { + mappingHelper.mapToResult( + OWNERSHIP_ASPECT_NAME, + (ermodelrelation, dataMap) -> + ermodelrelation.setOwnership(OwnershipMapper.map(new Ownership(dataMap), entityUrn))); + } + if (aspectMap != null && aspectMap.containsKey(STATUS_ASPECT_NAME)) { + mappingHelper.mapToResult( + STATUS_ASPECT_NAME, + (ermodelrelation, dataMap) -> + ermodelrelation.setStatus(StatusMapper.map(new Status(dataMap)))); + } + if (aspectMap != null && aspectMap.containsKey(GLOBAL_TAGS_ASPECT_NAME)) { + mappingHelper.mapToResult( + GLOBAL_TAGS_ASPECT_NAME, + (ermodelrelation, dataMap) -> this.mapGlobalTags(ermodelrelation, dataMap, entityUrn)); + } + if (aspectMap != null && aspectMap.containsKey(GLOSSARY_TERMS_ASPECT_NAME)) { + mappingHelper.mapToResult( + GLOSSARY_TERMS_ASPECT_NAME, + (ermodelrelation, dataMap) -> + ermodelrelation.setGlossaryTerms( + GlossaryTermsMapper.map(new GlossaryTerms(dataMap), entityUrn))); + } + return mappingHelper.getResult(); + } + + private void mapEditableProperties( + @Nonnull ERModelRelationship ermodelrelation, @Nonnull DataMap dataMap) { + final EditableERModelRelationshipProperties editableERModelRelationProperties = + new EditableERModelRelationshipProperties(dataMap); + ermodelrelation.setEditableProperties( + com.linkedin.datahub.graphql.generated.ERModelRelationshipEditableProperties.builder() + .setDescription(editableERModelRelationProperties.getDescription()) + .setName(editableERModelRelationProperties.getName()) + .build()); + } + + private void mapERModelRelationKey( + @Nonnull ERModelRelationship ermodelrelation, @Nonnull DataMap datamap) { + ERModelRelationshipKey ermodelrelationKey = new ERModelRelationshipKey(datamap); + ermodelrelation.setId(ermodelrelationKey.getId()); + } + + private void mapProperties( + @Nonnull ERModelRelationship ermodelrelation, @Nonnull DataMap dataMap) { + final ERModelRelationshipProperties ermodelrelationProperties = + new ERModelRelationshipProperties(dataMap); + ermodelrelation.setProperties( + com.linkedin.datahub.graphql.generated.ERModelRelationshipProperties.builder() + .setName(ermodelrelationProperties.getName()) + .setSource(createPartialDataset(ermodelrelationProperties.getSource())) + .setDestination(createPartialDataset(ermodelrelationProperties.getDestination())) + .setCreatedTime( + ermodelrelationProperties.hasCreated() + && ermodelrelationProperties.getCreated().getTime() > 0 + ? ermodelrelationProperties.getCreated().getTime() + : 0) + .setRelationshipFieldMappings( + ermodelrelationProperties.hasRelationshipFieldMappings() + ? this.mapERModelRelationFieldMappings(ermodelrelationProperties) + : null) + .build()); + + if (ermodelrelationProperties.hasCreated() + && Objects.requireNonNull(ermodelrelationProperties.getCreated()).hasActor()) { + ermodelrelation + .getProperties() + .setCreatedActor( + UrnToEntityMapper.map(ermodelrelationProperties.getCreated().getActor())); + } + } + + private Dataset createPartialDataset(@Nonnull Urn datasetUrn) { + + Dataset partialDataset = new Dataset(); + + partialDataset.setUrn(datasetUrn.toString()); + + return partialDataset; + } + + private List mapERModelRelationFieldMappings( + ERModelRelationshipProperties ermodelrelationProperties) { + final List relationshipFieldMappingList = new ArrayList<>(); + + ermodelrelationProperties + .getRelationshipFieldMappings() + .forEach( + relationshipFieldMapping -> + relationshipFieldMappingList.add( + this.mapRelationshipFieldMappings(relationshipFieldMapping))); + + return relationshipFieldMappingList; + } + + private com.linkedin.datahub.graphql.generated.RelationshipFieldMapping + mapRelationshipFieldMappings( + com.linkedin.ermodelrelation.RelationshipFieldMapping relationFieldMapping) { + return com.linkedin.datahub.graphql.generated.RelationshipFieldMapping.builder() + .setDestinationField(relationFieldMapping.getDestinationField()) + .setSourceField(relationFieldMapping.getSourceField()) + .build(); + } + + private void mapGlobalTags( + @Nonnull ERModelRelationship ermodelrelation, + @Nonnull DataMap dataMap, + @Nonnull final Urn entityUrn) { + com.linkedin.datahub.graphql.generated.GlobalTags globalTags = + GlobalTagsMapper.map(new GlobalTags(dataMap), entityUrn); + ermodelrelation.setTags(globalTags); + } +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/mappers/ERModelRelationshipUpdateInputMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/mappers/ERModelRelationshipUpdateInputMapper.java new file mode 100644 index 00000000000000..7c957bab77b68e --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/ermodelrelationship/mappers/ERModelRelationshipUpdateInputMapper.java @@ -0,0 +1,190 @@ +package com.linkedin.datahub.graphql.types.ermodelrelationship.mappers; + +import static com.linkedin.metadata.Constants.*; + +import com.linkedin.common.AuditStamp; +import com.linkedin.common.urn.DatasetUrn; +import com.linkedin.common.urn.Urn; +import com.linkedin.data.template.SetMode; +import com.linkedin.datahub.graphql.generated.ERModelRelationshipEditablePropertiesUpdate; +import com.linkedin.datahub.graphql.generated.ERModelRelationshipPropertiesInput; +import com.linkedin.datahub.graphql.generated.ERModelRelationshipUpdateInput; +import com.linkedin.datahub.graphql.generated.RelationshipFieldMappingInput; +import com.linkedin.datahub.graphql.types.common.mappers.util.UpdateMappingHelper; +import com.linkedin.datahub.graphql.types.mappers.InputModelMapper; +import com.linkedin.ermodelrelation.ERModelRelationshipCardinality; +import com.linkedin.ermodelrelation.ERModelRelationshipProperties; +import com.linkedin.ermodelrelation.EditableERModelRelationshipProperties; +import com.linkedin.ermodelrelation.RelationshipFieldMappingArray; +import com.linkedin.mxe.MetadataChangeProposal; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Nonnull; + +public class ERModelRelationshipUpdateInputMapper + implements InputModelMapper< + ERModelRelationshipUpdateInput, Collection, Urn> { + public static final ERModelRelationshipUpdateInputMapper INSTANCE = + new ERModelRelationshipUpdateInputMapper(); + + public static Collection map( + @Nonnull final ERModelRelationshipUpdateInput ermodelrelationUpdateInput, + @Nonnull final Urn actor) { + return INSTANCE.apply(ermodelrelationUpdateInput, actor); + } + + @Override + public Collection apply(ERModelRelationshipUpdateInput input, Urn actor) { + final Collection proposals = new ArrayList<>(8); + final UpdateMappingHelper updateMappingHelper = + new UpdateMappingHelper(ER_MODEL_RELATIONSHIP_ENTITY_NAME); + final long currentTime = System.currentTimeMillis(); + final AuditStamp auditstamp = new AuditStamp(); + auditstamp.setActor(actor, SetMode.IGNORE_NULL); + auditstamp.setTime(currentTime); + if (input.getProperties() != null) { + com.linkedin.ermodelrelation.ERModelRelationshipProperties ermodelrelationProperties = + createERModelRelationProperties(input.getProperties(), auditstamp); + proposals.add( + updateMappingHelper.aspectToProposal( + ermodelrelationProperties, ER_MODEL_RELATIONSHIP_PROPERTIES_ASPECT_NAME)); + } + if (input.getEditableProperties() != null) { + final EditableERModelRelationshipProperties editableERModelRelationProperties = + ermodelrelationshipEditablePropsSettings(input.getEditableProperties()); + proposals.add( + updateMappingHelper.aspectToProposal( + editableERModelRelationProperties, + EDITABLE_ER_MODEL_RELATIONSHIP_PROPERTIES_ASPECT_NAME)); + } + return proposals; + } + + private ERModelRelationshipProperties createERModelRelationProperties( + ERModelRelationshipPropertiesInput inputProperties, AuditStamp auditstamp) { + com.linkedin.ermodelrelation.ERModelRelationshipProperties ermodelrelationProperties = + new com.linkedin.ermodelrelation.ERModelRelationshipProperties(); + if (inputProperties.getName() != null) { + ermodelrelationProperties.setName(inputProperties.getName()); + } + try { + if (inputProperties.getSource() != null) { + ermodelrelationProperties.setSource( + DatasetUrn.createFromString(inputProperties.getSource())); + } + if (inputProperties.getDestination() != null) { + ermodelrelationProperties.setDestination( + DatasetUrn.createFromString(inputProperties.getDestination())); + } + } catch (URISyntaxException e) { + e.printStackTrace(); + } + + if (inputProperties.getRelationshipFieldmappings() != null) { + if (inputProperties.getRelationshipFieldmappings().size() > 0) { + com.linkedin.ermodelrelation.RelationshipFieldMappingArray relationshipFieldMappingsArray = + ermodelrelationFieldMappingSettings(inputProperties.getRelationshipFieldmappings()); + ermodelrelationProperties.setCardinality( + ermodelrelationCardinalitySettings(inputProperties.getRelationshipFieldmappings())); + ermodelrelationProperties.setRelationshipFieldMappings(relationshipFieldMappingsArray); + } + + if (inputProperties.getCreated() != null && inputProperties.getCreated()) { + ermodelrelationProperties.setCreated(auditstamp); + } else { + if (inputProperties.getCreatedBy() != null && inputProperties.getCreatedAt() != 0) { + final AuditStamp auditstampEdit = new AuditStamp(); + try { + auditstampEdit.setActor(Urn.createFromString(inputProperties.getCreatedBy())); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + auditstampEdit.setTime(inputProperties.getCreatedAt()); + ermodelrelationProperties.setCreated(auditstampEdit); + } + } + ermodelrelationProperties.setLastModified(auditstamp); + } + return ermodelrelationProperties; + } + + private com.linkedin.ermodelrelation.ERModelRelationshipCardinality + ermodelrelationCardinalitySettings( + List ermodelrelationFieldMapping) { + + Set sourceFields = new HashSet<>(); + Set destFields = new HashSet<>(); + AtomicInteger sourceCount = new AtomicInteger(); + AtomicInteger destCount = new AtomicInteger(); + + ermodelrelationFieldMapping.forEach( + relationshipFieldMappingInput -> { + sourceFields.add(relationshipFieldMappingInput.getSourceField()); + sourceCount.getAndIncrement(); + destFields.add(relationshipFieldMappingInput.getDestinationField()); + destCount.getAndIncrement(); + }); + + if (sourceFields.size() == sourceCount.get()) { + if (destFields.size() == destCount.get()) { + return ERModelRelationshipCardinality.ONE_ONE; + } else { + return ERModelRelationshipCardinality.N_ONE; + } + } else { + if (destFields.size() == destCount.get()) { + return ERModelRelationshipCardinality.ONE_N; + } else { + return ERModelRelationshipCardinality.N_N; + } + } + } + + private com.linkedin.ermodelrelation.RelationshipFieldMappingArray + ermodelrelationFieldMappingSettings( + List ermodelrelationFieldMapping) { + + List relationshipFieldMappingList = + this.mapRelationshipFieldMapping(ermodelrelationFieldMapping); + + return new RelationshipFieldMappingArray(relationshipFieldMappingList); + } + + private List mapRelationshipFieldMapping( + List ermodelrelationFieldMapping) { + + List relationshipFieldMappingList = + new ArrayList<>(); + + ermodelrelationFieldMapping.forEach( + relationshipFieldMappingInput -> { + com.linkedin.ermodelrelation.RelationshipFieldMapping relationshipFieldMapping = + new com.linkedin.ermodelrelation.RelationshipFieldMapping(); + relationshipFieldMapping.setSourceField(relationshipFieldMappingInput.getSourceField()); + relationshipFieldMapping.setDestinationField( + relationshipFieldMappingInput.getDestinationField()); + relationshipFieldMappingList.add(relationshipFieldMapping); + }); + + return relationshipFieldMappingList; + } + + private static EditableERModelRelationshipProperties ermodelrelationshipEditablePropsSettings( + ERModelRelationshipEditablePropertiesUpdate editPropsInput) { + final EditableERModelRelationshipProperties editableERModelRelationProperties = + new EditableERModelRelationshipProperties(); + if (editPropsInput.getName() != null && editPropsInput.getName().trim().length() > 0) { + editableERModelRelationProperties.setName(editPropsInput.getName()); + } + if (editPropsInput.getDescription() != null + && editPropsInput.getDescription().trim().length() > 0) { + editableERModelRelationProperties.setDescription(editPropsInput.getDescription()); + } + return editableERModelRelationProperties; + } +} diff --git a/datahub-graphql-core/src/main/resources/app.graphql b/datahub-graphql-core/src/main/resources/app.graphql index 7964f7e4fab238..39cff0f5114bfa 100644 --- a/datahub-graphql-core/src/main/resources/app.graphql +++ b/datahub-graphql-core/src/main/resources/app.graphql @@ -456,6 +456,10 @@ type FeatureFlagsConfig { Whether we should show CTAs in the UI related to moving to Managed DataHub by Acryl. """ showAcrylInfo: Boolean! + """ + Whether ERModelRelationship Tables Feature should be shown. + """ + erModelRelationshipFeatureEnabled: Boolean! """ Whether we should show AccessManagement tab in the datahub UI. diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index d1e727185657b9..c217620dbf6cd8 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -73,6 +73,11 @@ type Query { """ role(urn: String!): Role + """ + Fetch a ERModelRelationship by primary key (urn) + """ + erModelRelationship(urn: String!): ERModelRelationship + """ Fetch a Glossary Term by primary key (urn) """ @@ -233,6 +238,140 @@ type Query { dataPlatformInstance(urn: String!): DataPlatformInstance } +""" +An ERModelRelationship is a high-level abstraction that dictates what datasets fields are erModelRelationshiped. +""" +type ERModelRelationship implements EntityWithRelationships & Entity { + """ + The primary key of the role + """ + urn: String! + + """ + The standard Entity Type + """ + type: EntityType! + + """ + Unique id for the erModelRelationship + """ + id: String! + + """ + An additional set of read only properties + """ + properties: ERModelRelationshipProperties + + """ + An additional set of of read write properties + """ + editableProperties: ERModelRelationshipEditableProperties + + """ + References to internal resources related to the dataset + """ + institutionalMemory: InstitutionalMemory + + """ + Ownership metadata of the dataset + """ + ownership: Ownership + + """ + Status of the Dataset + """ + status: Status + + """ + Tags used for searching dataset + """ + tags: GlobalTags + + """ + The structured glossary terms associated with the dataset + """ + glossaryTerms: GlossaryTerms + + """ + List of relationships between the source Entity and some destination entities with a given types + """ + relationships(input: RelationshipsInput!): EntityRelationshipsResult + + """ + Privileges given to a user relevant to this entity + """ + privileges: EntityPrivileges + + """ + No-op required for the model + """ + lineage(input: LineageInput!): EntityLineageResult +} + +""" +Additional properties about a ERModelRelationship +""" +type ERModelRelationshipEditableProperties { + + """ + Documentation of the ERModelRelationship + """ + description: String + """ + Display name of the ERModelRelationship + """ + name: String +} + +""" +Additional properties about a ERModelRelationship +""" +type ERModelRelationshipProperties { + + """ + The name of the ERModelRelationship used in display + """ + name: String! + """ + The urn of source + """ + source: Dataset! + + """ + The urn of destination + """ + destination: Dataset! + + """ + The relationFieldMappings + """ + relationshipFieldMappings: [RelationshipFieldMapping!] + + """ + Created timestamp millis associated with the ERModelRelationship + """ + createdTime: Long + + """ + Created actor urn associated with the ERModelRelationship + """ + createdActor: Entity +} + +""" +ERModelRelationship FieldMap +""" +type RelationshipFieldMapping { + """ + left field + """ + sourceField: String! + """ + bfield + """ + destinationField: String! +} + """ Root type used for updating DataHub Metadata Coming soon createEntity, addOwner, removeOwner mutations @@ -467,6 +606,31 @@ type Mutation { """ unsetDomain(entityUrn: String!): Boolean + """ + Create a ERModelRelationship + """ + createERModelRelationship( + "Input required to create a new ERModelRelationship" + input: ERModelRelationshipUpdateInput!): ERModelRelationship + + """ + Update a ERModelRelationship + """ + updateERModelRelationship( + "The urn of the ERModelRelationship to update" + urn: String!, + "Input required to updat an existing DataHub View" + input: ERModelRelationshipUpdateInput!): Boolean + + """ + Delete a ERModelRelationship + """ + deleteERModelRelationship( + "The urn of the ERModelRelationship to delete" + urn: String!): Boolean + + + """ Sets the Deprecation status for a Metadata Entity. Requires the Edit Deprecation status privilege for an entity. """ @@ -786,6 +950,11 @@ enum EntityType { """ DATA_PLATFORM + """ + The ERModelRelationship Entity + """ + ER_MODEL_RELATIONSHIP + """ The Dashboard Entity """ @@ -4543,6 +4712,21 @@ input DatasetEditablePropertiesUpdate { description: String! } +""" +Update to writable Dataset fields +""" +input ERModelRelationshipEditablePropertiesUpdate { + """ + Display name of the ERModelRelationship + """ + name: String + + """ + Writable description for ERModelRelationship + """ + description: String! +} + """ Update to writable Chart fields """ @@ -4661,6 +4845,68 @@ input CreateTagInput { description: String } +""" +Input required to create/update a new ERModelRelationship +""" +input ERModelRelationshipUpdateInput { + """ + Details about the ERModelRelationship + """ + properties: ERModelRelationshipPropertiesInput + """ + Update to editable properties + """ + editableProperties: ERModelRelationshipEditablePropertiesUpdate +} + +""" +Details about the ERModelRelationship +""" +input ERModelRelationshipPropertiesInput { + """ + Details about the ERModelRelationship + """ + name: String! + """ + Details about the ERModelRelationship + """ + source: String! + """ + Details about the ERModelRelationship + """ + destination: String! + """ + Details about the ERModelRelationship + """ + relationshipFieldmappings: [RelationshipFieldMappingInput!] + """ + optional flag about the ERModelRelationship is getting create + """ + created: Boolean + """ + optional field to prevent created time while the ERModelRelationship is getting update + """ + createdAt: Long + """ + optional field to prevent create actor while the ERModelRelationship is getting update + """ + createdBy: String +} + +""" +Details about the ERModelRelationship +""" +input RelationshipFieldMappingInput { + """ + Details about the ERModelRelationship + """ + sourceField: String + """ + Details about the ERModelRelationship + """ + destinationField: String +} + """ An update for the ownership information for a Metadata Entity """ diff --git a/datahub-web-react/src/app/buildEntityRegistry.ts b/datahub-web-react/src/app/buildEntityRegistry.ts index fdd8e0afa77d1d..8e16bd52c62b17 100644 --- a/datahub-web-react/src/app/buildEntityRegistry.ts +++ b/datahub-web-react/src/app/buildEntityRegistry.ts @@ -19,6 +19,7 @@ import GlossaryNodeEntity from './entity/glossaryNode/GlossaryNodeEntity'; import { DataPlatformEntity } from './entity/dataPlatform/DataPlatformEntity'; import { DataProductEntity } from './entity/dataProduct/DataProductEntity'; import { DataPlatformInstanceEntity } from './entity/dataPlatformInstance/DataPlatformInstanceEntity'; +import { ERModelRelationshipEntity } from './entity/ermodelrelationships/ERModelRelationshipEntity' import { RoleEntity } from './entity/Access/RoleEntity'; import { RestrictedEntity } from './entity/restricted/RestrictedEntity'; @@ -45,6 +46,7 @@ export default function buildEntityRegistry() { registry.register(new DataPlatformEntity()); registry.register(new DataProductEntity()); registry.register(new DataPlatformInstanceEntity()); + registry.register(new ERModelRelationshipEntity()) registry.register(new RestrictedEntity()); return registry; } \ No newline at end of file diff --git a/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx b/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx index 0758cc41a7e413..d9dc6efa1a76a9 100644 --- a/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx +++ b/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx @@ -31,6 +31,7 @@ import { EmbedTab } from '../shared/tabs/Embed/EmbedTab'; import EmbeddedProfile from '../shared/embed/EmbeddedProfile'; import DataProductSection from '../shared/containers/profile/sidebar/DataProduct/DataProductSection'; import { getDataProduct } from '../shared/utils'; +import { RelationshipsTab } from '../shared/tabs/Dataset/Relationship/RelationshipsTab'; import AccessManagement from '../shared/tabs/Dataset/AccessManagement/AccessManagement'; import { matchedFieldPathsRenderer } from '../../search/matches/matchedFieldPathsRenderer'; import { getLastUpdatedMs } from './shared/utils'; @@ -105,6 +106,14 @@ export class DatasetEntity implements Entity { name: 'Schema', component: SchemaTab, }, + { + name: 'Relationships', + component: RelationshipsTab, + display: { + visible: (_, _1) => false, + enabled: (_, _2) => false, + }, + }, { name: 'View Definition', component: ViewDefinitionTab, diff --git a/datahub-web-react/src/app/entity/ermodelrelationships/ERModelRelationshipEntity.tsx b/datahub-web-react/src/app/entity/ermodelrelationships/ERModelRelationshipEntity.tsx new file mode 100644 index 00000000000000..91005f17b80c72 --- /dev/null +++ b/datahub-web-react/src/app/entity/ermodelrelationships/ERModelRelationshipEntity.tsx @@ -0,0 +1,141 @@ +import * as React from 'react'; +import { DatabaseOutlined, DatabaseFilled } from '@ant-design/icons'; +import { EntityType, ErModelRelationship, OwnershipType, SearchResult } from '../../../types.generated'; +import { Entity, IconStyleType, PreviewType } from '../Entity'; +import { getDataForEntityType } from '../shared/containers/profile/utils'; +import { GenericEntityProperties } from '../shared/types'; +import { ERModelRelationshipPreviewCard } from './preview/ERModelRelationshipPreviewCard'; +import ermodelrelationshipIcon from '../../../images/ermodelrelationshipIcon.svg'; +import { ERModelRelationshipTab } from '../shared/tabs/ERModelRelationship/ERModelRelationshipTab'; +import { useGetErModelRelationshipQuery, useUpdateErModelRelationshipMutation } from '../../../graphql/ermodelrelationship.generated'; +import { DocumentationTab } from '../shared/tabs/Documentation/DocumentationTab'; +import { PropertiesTab } from '../shared/tabs/Properties/PropertiesTab'; +import { SidebarAboutSection } from '../shared/containers/profile/sidebar/AboutSection/SidebarAboutSection'; +import { SidebarTagsSection } from '../shared/containers/profile/sidebar/SidebarTagsSection'; +import { EntityProfile } from '../shared/containers/profile/EntityProfile'; +import './preview/ERModelRelationshipAction.less'; +import { SidebarOwnerSection } from '../shared/containers/profile/sidebar/Ownership/sidebar/SidebarOwnerSection'; + +/** + * Definition of the DataHub ErModelRelationship entity. + */ + +export class ERModelRelationshipEntity implements Entity { + type: EntityType = EntityType.ErModelRelationship; + + icon = (fontSize: number, styleType: IconStyleType) => { + if (styleType === IconStyleType.TAB_VIEW) { + return ; + } + + if (styleType === IconStyleType.HIGHLIGHT) { + return ; + } + + if (styleType === IconStyleType.SVG) { + return ( + + ); + } + + return ; + }; + + isSearchEnabled = () => true; + + isBrowseEnabled = () => false; + + isLineageEnabled = () => false; + + getAutoCompleteFieldName = () => 'name'; + + getPathName = () => 'erModelRelationship'; + + getCollectionName = () => ''; + + getEntityName = () => 'ER-Model-Relationship'; + + renderProfile = (urn: string) => ( + + ); + + getOverridePropertiesFromEntity = (_ermodelrelation?: ErModelRelationship | null): GenericEntityProperties => { + return {}; + }; + + renderPreview = (_: PreviewType, data: ErModelRelationship) => { + return ( + <> + {data.properties?.name || data.editableProperties?.name || ''} + } + description={data?.editableProperties?.description || ''} + owners={data.ownership?.owners} + glossaryTerms={data?.glossaryTerms || undefined} + globalTags={data?.tags} + /> + + ); + }; + + renderSearch = (result: SearchResult) => { + return this.renderPreview(PreviewType.SEARCH, result.entity as ErModelRelationship); + }; + + displayName = (data: ErModelRelationship) => { + return data.properties?.name || data.editableProperties?.name || data.urn; + }; + + getGenericEntityProperties = (data: ErModelRelationship) => { + return getDataForEntityType({ + data, + entityType: this.type, + getOverrideProperties: this.getOverridePropertiesFromEntity, + }); + }; + + supportedCapabilities = () => { + return new Set([]); + }; +} diff --git a/datahub-web-react/src/app/entity/ermodelrelationships/preview/ERModelRelationshipAction.less b/datahub-web-react/src/app/entity/ermodelrelationships/preview/ERModelRelationshipAction.less new file mode 100644 index 00000000000000..7ac539d7a6a1e2 --- /dev/null +++ b/datahub-web-react/src/app/entity/ermodelrelationships/preview/ERModelRelationshipAction.less @@ -0,0 +1,12 @@ +@import "../../../../../node_modules/antd/dist/antd.less"; + +.joinName { + width: 385px; + height: 24px; + font-style: normal; + font-weight: 700; + font-size: 16px; + line-height: 24px; + align-items: center; + color: #262626; +} \ No newline at end of file diff --git a/datahub-web-react/src/app/entity/ermodelrelationships/preview/ERModelRelationshipPreviewCard.tsx b/datahub-web-react/src/app/entity/ermodelrelationships/preview/ERModelRelationshipPreviewCard.tsx new file mode 100644 index 00000000000000..33669485f18c61 --- /dev/null +++ b/datahub-web-react/src/app/entity/ermodelrelationships/preview/ERModelRelationshipPreviewCard.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Card, Collapse } from 'antd'; +import ermodelrelationshipIcon from '../../../../images/ermodelrelationshipIcon.svg'; +import { EntityType, Owner, GlobalTags, GlossaryTerms } from '../../../../types.generated'; +import { useEntityRegistry } from '../../../useEntityRegistry'; +import DefaultPreviewCard from '../../../preview/DefaultPreviewCard'; +import { IconStyleType } from '../../Entity'; + +const { Panel } = Collapse; + +export const ERModelRelationshipPreviewCard = ({ + urn, + name, + owners, + description, + globalTags, + glossaryTerms, +}: { + urn: string; + name: string | any; + description: string | any; + globalTags?: GlobalTags | null; + glossaryTerms?: GlossaryTerms | null; + owners?: Array | null; +}): JSX.Element => { + const entityRegistry = useEntityRegistry(); + const getERModelRelationHeader = (): JSX.Element => { + return ( +

+ } + tags={globalTags || undefined} + glossaryTerms={glossaryTerms || undefined} + owners={owners} + type="ERModelRelationship" + typeIcon={entityRegistry.getIcon(EntityType.ErModelRelationship, 14, IconStyleType.ACCENT)} + titleSizePx={18} + /> +
+ ); + }; + + return ( + <> + + + + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/CreateERModelRelationModal.less b/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/CreateERModelRelationModal.less new file mode 100644 index 00000000000000..b50d3debaf1efd --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/CreateERModelRelationModal.less @@ -0,0 +1,294 @@ +@import "../../../../../../../node_modules/antd/dist/antd.less"; + +.CreateERModelRelationModal { + .ermodelrelation-name { + padding: 8px 16px; + width: 948.5px !important; + height: 40px !important; + background: #FFFFFF; + border: 1px solid #D9D9D9; + box-shadow: 0px 2px 0px rgba(0, 0, 0, 0.016); + border-radius: 2px; + align-items: center; + } + .ant-select-single.ant-select-lg:not(.ant-select-customize-input) .ant-select-selector { + align-items: center; + padding: 8px 16px; + gap: 8px; + max-width: 370px; + min-width: 370px; + height: 38px; + background: #FFFFFF; + border: 1px solid #D9D9D9; + box-shadow: 0px 2px 0px rgba(0, 0, 0, 0.016); + border-radius: 2px; + } + .ant-modal-content { + box-sizing: border-box; + width: 1000px; + height: 765px; + background: #FFFFFF; + border: 1px solid #ADC0D7; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.15); + border-radius: 8px; + left: -215px; + top: -55px; + } + .inner-div { + width: 970px; + height: 640px; + overflow-y: scroll; + margin-top: -20px; + overflow-x: hidden; + } + .ant-modal-header { + padding-top: 32px; + padding-left: 16px; + border-bottom: 0px !important; + } + + .ermodelrelation-title { + width: 300px !important; + height: 22px; + font-family: 'Arial'; + font-style: normal; + font-weight: 700; + font-size: 20px; + line-height: 22px; + color: #000000; + padding-top: 4px; + } + .all-content-heading{ + width: 380px; + height: 16px; + margin-top: 16px; + margin-bottom: 8px; + font-family: 'Arial'; + font-style: normal; + font-weight: 700; + font-size: 14px; + line-height: 16px; + color: #1B2F41; + flex: none; + } + .all-table-heading{ + width: 380px; + height: 16px; + margin-bottom: 8px; + font-family: 'Arial'; + font-style: normal; + font-weight: 700; + font-size: 14px; + line-height: 16px; + color: #1B2F41; + flex: none; + } + + .field-heading{ + height: 16px; + margin-top: 32px; + margin-bottom: 8px; + font-family: 'Arial'; + font-style: normal; + font-weight: 700; + font-size: 14px; + line-height: 16px; + color: #1B2F41; + } + .all-information{ + width: 680px; + height: 24px; + font-family: 'Arial'; + font-style: normal; + font-weight: 400; + font-size: 16px; + color: #1B2F41; + } + .techNameDisplay { + font-size: 14px; + font-style: normal; + font-weight: 400; + color: #595959; + } + .instructions-list { + width: 774px; + height: 220px; + font-family: 'Arial'; + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 150%; + color: #556573; + flex: none; + } + .ant-modal-footer { + padding-top: 0px; + padding-bottom: 10px; + padding-right: 25px; + border-top: 0px; + } + + .ant-btn-link { + padding-left: 0px !important; + padding-right: 1px !important; + font-family: 'Arial' !important; + font-style: normal !important; + font-weight: 400 !important; + font-size: 14px !important; + color: #1890FF !important; + } + .add-btn-link { + padding-left: 865px !important; + padding-right: 8px !important; + padding-top: 16px !important; + height: 20px; + font-family: 'Arial' !important; + font-style: normal !important; + font-weight: 700 !important; + font-size: 12px !important; + color: #1890FF !important; + line-height: 20px; + } + + .cancel-btn { + box-sizing: border-box; + margin-left: 440px; + width: 85px; + height: 40px !important; + background: #FFFFFF; + border: 1px solid #D9D9D9 !important; + border-radius: 5px; + color: #262626; + } + + .submit-btn, .submit-btn:hover { + margin-left: 28px; + //margin-top: 6px; + width: 86px; + height: 40px; + background: #1890FF; + border: none; + color: #FFFFFF; + } + .footer-parent-div { + padding-left: 8px; + display: flex; + } + .ermodelrelation-select-selector { + align-items: center; + width: 300px !important; + height: 38px !important; + border: none; + max-width: 373px !important; + min-width: 373px !important; + font-size: 14px; + line-height: 22px; + font-family: 'Roboto Mono',monospace; + font-weight: 400; + background: white; + font-style: normal; + color: #000000D9; + } + .ermodelrelation-details-ta { + height: 95px; + width: 720px; + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 22px; + color: rgba(0, 0, 0, 0.85); + } + .ERModelRelationTable { + .icon-image { + box-sizing: border-box; + width: 16px; + height: 0px; + border: 1px solid #000000; + } + .ant-table-content { + width: 950px; + } + .ant-table-thead > tr th { + font-style: normal; + font-weight: 500; + font-size: 14px; + line-height: 22px; + color: #1B2F41; + align-items: center; + padding: 16px; + gap: 4px; + isolation: isolate; + height: 56px !important; + background: #FFFFFF; + border-color: rgba(0, 0, 0, 0.12); + } + .ant-table-tbody > tr td { + letter-spacing: 0.3px; + margin-left: 0px; + background: white; + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 22px; + color: rgba(0, 0, 0, 0.85); + border-color: rgba(0, 0, 0, 0.12); + } + td:nth-child(1), td:nth-child(3){ + max-width: 400px !important; + min-width: 400px !important; + } + .titleNameDisplay{ + max-width: 360px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + width: fit-content; + display: inline-block; + font-size: 14px; + padding: 4px 0; + } + .firstRow{ + display: flex; + justify-content: left; + } + + .editableNameDisplay { + display: block; + overflow-wrap: break-word; + white-space: nowrap; + max-width: 360px; + overflow: hidden; + text-overflow: ellipsis; + height: 16px; + font-family: 'Arial'; + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 16px; + color: #595959; + } + td:nth-child(2), th:nth-child(2){ + min-width: 44px !important; + max-width: 44px !important; + } + td:nth-child(4), th:nth-child(4){ + min-width: 75px !important; + max-width: 75px !important; + } + table { + border-radius: 0.375rem; + border-collapse: collapse; + } + .SelectedRow { + background-color: #ECF2F8; + } + } +} +.cancel-modal { + .ant-btn-primary { + color: #FFFFFF; + background: #1890FF; + border: none; + box-shadow: none; + } +} \ No newline at end of file diff --git a/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/CreateERModelRelationModal.tsx b/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/CreateERModelRelationModal.tsx new file mode 100644 index 00000000000000..a6f84b8c8fc5ca --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/CreateERModelRelationModal.tsx @@ -0,0 +1,409 @@ +import React, { useState } from 'react'; +import { Button, Form, Input, message, Modal, Table } from 'antd'; +import TextArea from 'antd/lib/input/TextArea'; +import { PlusOutlined } from '@ant-design/icons'; +import arrow from '../../../../../../images/Arrow.svg'; +import './CreateERModelRelationModal.less'; +import { EntityType, ErModelRelationship, OwnerEntityType } from '../../../../../../types.generated'; +import { useCreateErModelRelationshipMutation, useUpdateErModelRelationshipMutation } from '../../../../../../graphql/ermodelrelationship.generated'; +import { useUserContext } from '../../../../../context/useUserContext'; +import { EditableRow } from './EditableRow'; +import { EditableCell } from './EditableCell'; +import { checkDuplicateERModelRelation, getDatasetName, ERModelRelationDataType, validateERModelRelation } from './ERModelRelationUtils'; +import { useGetSearchResultsQuery } from '../../../../../../graphql/search.generated'; +import { useAddOwnerMutation } from '../../../../../../graphql/mutations.generated'; + +type Props = { + table1?: any; + table1Schema?: any; + table2?: any; + table2Schema?: any; + visible: boolean; + setModalVisible?: any; + onCancel: () => void; + editERModelRelation?: ErModelRelationship; + isEditing?: boolean; + refetch: () => Promise; +}; + +type EditableTableProps = Parameters[0]; +type ColumnTypes = Exclude; + +export const CreateERModelRelationModal = ({ + table1, + table1Schema, + table2, + table2Schema, + visible, + setModalVisible, + onCancel, + editERModelRelation, + isEditing, + refetch, +}: Props) => { + const [form] = Form.useForm(); + const { user } = useUserContext(); + const ownerEntityType = + user && user.type === EntityType.CorpGroup ? OwnerEntityType.CorpGroup : OwnerEntityType.CorpUser; + const table1Dataset = editERModelRelation?.properties?.source || table1?.dataset; + const table1DatasetSchema = editERModelRelation?.properties?.source || table1Schema; + const table2Dataset = editERModelRelation?.properties?.destination || table2?.dataset; + const table2DatasetSchema = editERModelRelation?.properties?.destination || table2Schema?.dataset; + + const [details, setDetails] = useState(editERModelRelation?.editableProperties?.description || ''); + const [ermodelrelationName, setERModelRelationName] = useState( + editERModelRelation?.editableProperties?.name || editERModelRelation?.properties?.name || editERModelRelation?.id || '', + ); + const [tableData, setTableData] = useState( + editERModelRelation?.properties?.relationshipFieldMappings?.map((item, index) => { + return { + key: index, + field1Name: item.sourceField, + field2Name: item.destinationField, + }; + }) || [ + { key: '0', field1Name: '', field2Name: '' }, + { key: '1', field1Name: '', field2Name: '' }, + ], + ); + const [count, setCount] = useState(editERModelRelation?.properties?.relationshipFieldMappings?.length || 2); + const [createMutation] = useCreateErModelRelationshipMutation(); + const [updateMutation] = useUpdateErModelRelationshipMutation(); + const [addOwnerMutation] = useAddOwnerMutation(); + const { refetch: getSearchResultsERModelRelations } = useGetSearchResultsQuery({ + skip: true, + }); + + const handleDelete = (record) => { + const newData = tableData.filter((item) => item.key !== record.key); + setTableData(newData); + }; + const onCancelSelect = () => { + Modal.confirm({ + title: `Exit`, + className: 'cancel-modal', + content: `Are you sure you want to exit? The changes made to the erModelRelationship will not be applied.`, + onOk() { + setERModelRelationName(editERModelRelation?.properties?.name || ''); + setDetails(editERModelRelation?.editableProperties?.description || ''); + setTableData( + editERModelRelation?.properties?.relationshipFieldMappings?.map((item, index) => { + return { + key: index, + field1Name: item.sourceField, + field2Name: item.destinationField, + }; + }) || [ + { key: '0', field1Name: '', field2Name: '' }, + { key: '1', field1Name: '', field2Name: '' }, + ], + ); + setCount(editERModelRelation?.properties?.relationshipFieldMappings?.length || 2); + onCancel?.(); + }, + onCancel() {}, + okText: 'Yes', + maskClosable: true, + closable: true, + }); + }; + const createERModelRelationship = () => { + createMutation({ + variables: { + input: { + properties: { + source: table1Dataset?.urn || '', + destination: table2Dataset?.urn || '', + name: ermodelrelationName, + relationshipFieldmappings: tableData.map((r) => { + return { + sourceField: r.field1Name, + destinationField: r.field2Name, + }; + }), + created: true, + }, + editableProperties: { + name: ermodelrelationName, + description: details, + }, + }, + }, + }) + .then(({ data }) => { + message.loading({ + content: 'Create...', + duration: 2, + }); + setTimeout(() => { + refetch(); + message.success({ + content: `ERModelRelation created!`, + duration: 2, + }); + }, 2000); + addOwnerMutation({ + variables: { + input: { + ownerUrn: user?.urn || '', + resourceUrn: data?.createERModelRelationship?.urn || '', + ownershipTypeUrn: 'urn:li:ownershipType:__system__technical_owner', + ownerEntityType: ownerEntityType || EntityType, + }, + }, + }); + }) + .catch((e) => { + message.destroy(); + message.error({ content: `Failed to create erModelRelationship: ${e.message || ''}`, duration: 3 }); + }); + }; + const originalERModelRelationName = editERModelRelation?.properties?.name; + const updateERModelRelationship = () => { + updateMutation({ + variables: { + urn: editERModelRelation?.urn || '', + input: { + properties: { + source: table1Dataset?.urn || '', + destination: table2Dataset?.urn || '', + name: originalERModelRelationName || '', + createdBy: editERModelRelation?.properties?.createdActor?.urn || user?.urn, + createdAt: editERModelRelation?.properties?.createdTime || 0, + relationshipFieldmappings: tableData.map((r) => { + return { + sourceField: r.field1Name, + destinationField: r.field2Name, + }; + }), + }, + editableProperties: { + name: ermodelrelationName, + description: details, + }, + }, + }, + }) + .then(() => { + message.loading({ + content: 'updating...', + duration: 2, + }); + setTimeout(() => { + refetch(); + message.success({ + content: `ERModelRelation updated!`, + duration: 2, + }); + }, 2000); + }) + .catch((e) => { + message.destroy(); + message.error({ content: `Failed to update erModelRelationship: ${e.message || ''}`, duration: 3 }); + }); + }; + const onSubmit = async () => { + const errors = validateERModelRelation(ermodelrelationName, tableData, isEditing, getSearchResultsERModelRelations); + if ((await errors).length > 0) { + const err = (await errors).join(`, `); + message.error(err); + return; + } + if (isEditing) { + updateERModelRelationship(); + } else { + createERModelRelationship(); + setERModelRelationName(''); + setDetails(''); + setTableData([ + { key: '0', field1Name: '', field2Name: '' }, + { key: '1', field1Name: '', field2Name: '' }, + ]); + setCount(2); + } + setModalVisible(false); + }; + + const table1NameBusiness = getDatasetName(table1Dataset); + const table1NameTech = table1Dataset?.name || table1Dataset?.urn.split(',').at(1) || ''; + const table2NameBusiness = getDatasetName(table2Dataset); + const table2NameTech = table2Dataset?.name || table2Dataset?.urn.split(',').at(1) || ''; + + const handleAdd = () => { + const newData: ERModelRelationDataType = { + key: count, + field1Name: '', + field2Name: '', + }; + setTableData([...tableData, newData]); + setCount(count + 1); + }; + const defaultColumns: (ColumnTypes[number] & { editable?: boolean; dataIndex: string; tableRecord?: any })[] = [ + { + title: ( +

+

+ {table1NameBusiness || table1NameTech} +
+
{table1NameTech !== table1NameBusiness && table1NameTech}
+

+ ), + dataIndex: 'field1Name', + tableRecord: table1DatasetSchema || {}, + editable: true, + }, + { + title: '', + dataIndex: '', + editable: false, + render: () => , + }, + { + title: ( +

+

+ {table2NameBusiness || table2NameTech} +
+
{table2NameTech !== table2NameBusiness && table2NameTech}
+

+ ), + dataIndex: 'field2Name', + tableRecord: table2DatasetSchema || {}, + editable: true, + }, + { + title: 'Action', + dataIndex: '', + editable: false, + render: (record) => + tableData.length > 1 ? ( + + ) : null, + }, + ]; + const handleSave = (row: ERModelRelationDataType) => { + const newData = [...tableData]; + const index = newData.findIndex((item) => row.key === item.key); + const item = newData[index]; + newData.splice(index, 1, { + ...item, + ...row, + }); + setTableData(newData); + }; + const components = { + body: { + row: EditableRow, + cell: EditableCell, + }, + }; + + const columns = defaultColumns.map((col) => { + if (!col.editable) { + return col; + } + return { + ...col, + onCell: (record: ERModelRelationDataType) => ({ + record, + editable: col.editable, + dataIndex: col.dataIndex, + tableRecord: col.tableRecord, + title: col.title, + handleSave, + }), + }; + }); + return ( + +

ER-Model-Relationship Parameters

+
+ +
+
+ +
+ + } + visible={visible} + closable={false} + className="CreateERModelRelationModal" + okButtonProps={{ hidden: true }} + cancelButtonProps={{ hidden: true }} + onCancel={onCancelSelect} + destroyOnClose + > +
+

Table 1

+

{table1NameBusiness}

+
{table1NameTech !== table1NameBusiness && table1NameTech}
+

Table 2

+

{table2NameBusiness}

+
{table2NameTech !== table2NameBusiness && table2NameTech}
+

ER-Model-Relationship name

+
+ + checkDuplicateERModelRelation(getSearchResultsERModelRelations, value?.trim()).then((result) => { + return result === true && !isEditing + ? Promise.reject( + new Error( + 'This ER-Model-Relationship name already exists. A unique name for each ER-Model-Relationship is required.', + ), + ) + : Promise.resolve(); + }), + }, + ]} + > + setERModelRelationName(e.target.value)} /> + +

Fields

+ + +

ER-Model-Relationship details

+ +