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
+
+
+
+
+
+ );
+};
diff --git a/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/ERModelRelationPreview.less b/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/ERModelRelationPreview.less
new file mode 100644
index 00000000000000..f71daad58d180a
--- /dev/null
+++ b/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/ERModelRelationPreview.less
@@ -0,0 +1,176 @@
+@import "../../../../../../../node_modules/antd/dist/antd.less";
+
+.ERModelRelationPreview {
+ .preview-main-div{
+ display: flex;
+ justify-content: space-between;
+ margin-left: 12px;
+ }
+ .div-edit-img img {
+ height: 15px;
+ line-height: 15px;
+ margin-top: 3px;
+ }
+ .div-view svg {
+ height: 20px;
+ line-height: 20px;
+ color: #1890FF;
+ font-weight: 500;
+ font-size: 12px;
+ }
+ .div-view-dataset svg {
+ color: #1890FF;
+ font-weight: 500;
+ font-size: 12px;
+ margin-top: -3px;
+ }
+ .div-edit-img {
+ display: flex;
+ height: 24px;
+ line-height: 24px;
+ margin-top: 18px;
+ margin-top: 0px;
+ }
+ .div-edit {
+ display: flex;
+ margin-left: 3px;
+ margin-right: 4px;
+ height: 24px;
+ font-weight: 700;
+ font-size: 12px;
+ line-height: 24px;
+ }
+ .btn-edit {
+ font-style: normal;
+ font-weight: 700 !important;
+ font-size: 12px !important;
+ line-height: 20px !important;
+ color: #262626 !important;
+ }
+ .div-view {
+ font-weight: 500;
+ font-size: 14px;
+ padding-top: 3px !important;
+ color: #1890FF;
+ display: inline-block;
+ margin: 0;
+ margin-left: -3px;
+ margin-top: 2px;
+ }
+ .div-view span{
+ font-weight: 500;
+ font-size: 14px;
+ color: #1890FF;
+ display: inline-block;
+ padding: 0;
+ margin: 0;
+ }
+ .ant-btn-link {
+ margin-top: -3px;
+ }
+ .div-view-dataset span{
+ font-weight: 500;
+ font-size: 14px;
+ color: #1890FF;
+ margin-top: -3px;
+ margin-right: -4px;
+ }
+
+ .all-content-heading{
+ height: 16px;
+ margin-bottom: 8px;
+ font-family: 'Arial';
+ font-style: normal;
+ font-weight: 700;
+ font-size: 14px;
+ line-height: 16px;
+ color: #1B2F41;
+ flex: none;
+ }
+
+ .all-content-info{
+ width: 720px;
+ font-style: normal;
+ font-weight: 400;
+ font-size: 14px !important;
+ line-height: 22px;
+ color: rgba(0, 0, 0, 0.85);
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+
+ .all-table-heading{
+ font-style: normal;
+ font-weight: 700;
+ width: fit-content;
+ font-family: 'Arial';
+ font-style: normal;
+ font-size: 16px;
+ line-height: 19px;
+ color: #1B2F41;
+ margin: 10px 0;
+ }
+ .extra-margin-rev {
+ margin-left: 4px;
+ }
+ .ERModelRelationTable {
+ width: 100%;
+ .ant-table-bordered{
+ margin-bottom: 12px;
+ margin-right: 12px;
+ }
+ .ant-table-thead > tr th {
+ padding: 16px;
+ gap: 4px;
+ background: #FFFFFF;
+ border-width: 1px 0px 1px 1px;
+ border-style: solid;
+ border-color: rgba(0, 0, 0, 0.12);
+ line-height: 22px;
+ height: 79px;
+ }
+ .ant-table-tbody > tr td {
+ font-size: 14px;
+ line-height: 22px;
+ font-family: 'Roboto Mono',monospace;
+ font-weight: 400;
+ background: white;
+ font-style: normal;
+ color: #000000D9;
+ padding: 16px;
+ border-width: 0px 0px 1px 1px;
+ border-style: solid;
+ border-color: rgba(0, 0, 0, 0.12);
+ }
+ .firstRow{
+ display: flex;
+ justify-content: left;
+ }
+ .titleNameDisplay {
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: 22px;
+ color: #1B2F41;
+ }
+ .editableNameDisplay {
+ color: #595959;
+ font-family: Arial;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ }
+ .SelectedRow {
+ background-color: #ECF2F8;
+ }
+ }
+}
+.radioButton{
+ .ant-radio-inner{
+ margin-top: 20px;
+ border-color: #4a5568;
+ }
+}
+
diff --git a/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/ERModelRelationPreview.tsx b/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/ERModelRelationPreview.tsx
new file mode 100644
index 00000000000000..a033f9d4a35741
--- /dev/null
+++ b/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/ERModelRelationPreview.tsx
@@ -0,0 +1,191 @@
+import React, { useState } from 'react';
+import { Button, Row, Table } from 'antd';
+import { RightOutlined } from '@ant-design/icons';
+import { useEntityRegistry } from '../../../../../useEntityRegistry';
+import arrow from '../../../../../../images/Arrow.svg';
+import editIcon from '../../../../../../images/editIconBlack.svg';
+import './ERModelRelationPreview.less';
+import { EntityType, ErModelRelationship } from '../../../../../../types.generated';
+import { CreateERModelRelationModal } from './CreateERModelRelationModal';
+import { getDatasetName } from './ERModelRelationUtils';
+
+type ERModelRelationRecord = {
+ sourceField: string;
+ destinationField: string;
+};
+type Props = {
+ ermodelrelationData: ErModelRelationship;
+ baseEntityUrn?: any;
+ prePageType?: string;
+ refetch: () => Promise;
+};
+type EditableTableProps = Parameters[0];
+type ColumnTypes = Exclude;
+export const ERModelRelationPreview = ({ ermodelrelationData, baseEntityUrn, prePageType, refetch }: Props) => {
+ const entityRegistry = useEntityRegistry();
+ const handleViewEntity = (entityType, urn) => {
+ const entityUrl = entityRegistry.getEntityUrl(entityType, urn);
+ window.open(entityUrl, '_blank');
+ };
+ const [modalVisible, setModalVisible] = useState(false);
+ const shuffleFlag = !(prePageType === 'Dataset' && baseEntityUrn === ermodelrelationData?.properties?.source?.urn);
+ const table1EditableName = shuffleFlag
+ ? getDatasetName(ermodelrelationData?.properties?.destination)
+ : getDatasetName(ermodelrelationData?.properties?.source);
+ const table2EditableName = shuffleFlag
+ ? getDatasetName(ermodelrelationData?.properties?.source)
+ : getDatasetName(ermodelrelationData?.properties?.destination);
+ const table1Name =
+ shuffleFlag && prePageType !== 'ERModelRelationship'
+ ? ermodelrelationData?.properties?.destination?.name
+ : ermodelrelationData?.properties?.source?.name;
+ const table2Name =
+ shuffleFlag && prePageType !== 'ERModelRelationship'
+ ? ermodelrelationData?.properties?.source?.name
+ : ermodelrelationData?.properties?.destination?.name;
+ const table1Urn =
+ shuffleFlag && prePageType !== 'ERModelRelationship'
+ ? ermodelrelationData?.properties?.destination?.urn
+ : ermodelrelationData?.properties?.source?.urn;
+ const table2Urn =
+ shuffleFlag && prePageType !== 'ERModelRelationship'
+ ? ermodelrelationData?.properties?.source?.urn
+ : ermodelrelationData?.properties?.destination?.urn;
+ const ermodelrelationHeader = ermodelrelationData?.editableProperties?.name || ermodelrelationData?.properties?.name || '';
+ function getFieldMap(): ERModelRelationRecord[] {
+ const newData = [] as ERModelRelationRecord[];
+ if (shuffleFlag && prePageType !== 'ERModelRelationship') {
+ ermodelrelationData?.properties?.relationshipFieldMappings?.map((item) => {
+ return newData.push({
+ sourceField: item.destinationField,
+ destinationField: item.sourceField,
+ });
+ });
+ } else {
+ ermodelrelationData?.properties?.relationshipFieldMappings?.map((item) => {
+ return newData.push({
+ sourceField: item.sourceField,
+ destinationField: item.destinationField,
+ });
+ });
+ }
+ return newData;
+ }
+ const columns = [
+ {
+ title: (
+
+
+
{table1EditableName || table1Name}
+
+ {prePageType === 'ERModelRelationship' && (
+
+ )}
+
+
+
+ {table1Name !== table1EditableName && table1Name}
+
+ ),
+ dataIndex: 'sourceField',
+ width: '48%',
+ sorter: ({ sourceField: a }, { sourceField: b }) => a.localeCompare(b),
+ },
+ {
+ title: '',
+ dataIndex: '',
+ width: '4%',
+ render: () => ,
+ },
+ {
+ title: (
+
+
+
{table2EditableName || table2Name}
+
+
+
+
+ {table2Name !== table2EditableName && table2Name}
+
+ ),
+ width: '48%',
+ dataIndex: 'destinationField',
+ sorter: ({ destinationField: a }, { destinationField: b }) => a.localeCompare(b),
+ },
+ ];
+
+ return (
+
+ {ermodelrelationData?.properties?.relationshipFieldMappings !== undefined && (
+
{
+ setModalVisible(false);
+ }}
+ editERModelRelation={ermodelrelationData}
+ isEditing
+ refetch={refetch}
+ />
+ )}
+
+
+ {(prePageType === 'Dataset' || ermodelrelationHeader !== ermodelrelationData?.properties?.name) && (
+
+ {ermodelrelationHeader}
+ {prePageType === 'Dataset' && (
+
+ )}
+
+ )}
+
+
+
+
+
+
+
+
+ {prePageType === 'Dataset' && (
+
+
ER-Model-Relationship details
+
{ermodelrelationData?.editableProperties?.description}
+
+ )}
+
+ );
+};
diff --git a/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/ERModelRelationUtils.tsx b/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/ERModelRelationUtils.tsx
new file mode 100644
index 00000000000000..d6bc1eb4c13152
--- /dev/null
+++ b/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/ERModelRelationUtils.tsx
@@ -0,0 +1,63 @@
+import React from 'react';
+import { FormInstance } from 'antd';
+import { EntityType } from '../../../../../../types.generated';
+
+export const EditableContext = React.createContext | null>(null);
+export interface ERModelRelationDataType {
+ key: React.Key;
+ field1Name: string;
+ field2Name: string;
+}
+
+export const checkDuplicateERModelRelation = async (getSearchResultsERModelRelations, name): Promise => {
+ const { data: searchResults } = await getSearchResultsERModelRelations({
+ input: {
+ type: EntityType.ErModelRelationship,
+ query: '',
+ orFilters: [
+ {
+ and: [
+ {
+ field: 'name',
+ values: [name],
+ },
+ ],
+ },
+ ],
+ start: 0,
+ count: 1000,
+ },
+ });
+ const recordExists = searchResults && searchResults.search && searchResults.search.total > 0;
+ return recordExists || false;
+};
+const validateTableData = (fieldMappingData: ERModelRelationDataType) => {
+ if (fieldMappingData.field1Name !== '' && fieldMappingData.field2Name !== '') {
+ return true;
+ }
+ return false;
+};
+export const validateERModelRelation = async (nameField: string, tableSchema: ERModelRelationDataType[], editFlag, getSearchResultsERModelRelations) => {
+ const errors: string[] = [];
+ const bDuplicateName = await checkDuplicateERModelRelation(getSearchResultsERModelRelations, nameField?.trim()).then((result) => result);
+ if (nameField === '') {
+ errors.push('ER-Model-Relationship name is required');
+ }
+ if (bDuplicateName && !editFlag) {
+ errors.push('This ER-Model-Relationship name already exists. A unique name for each ER-Model-Relationship is required');
+ }
+ const faultyRows = tableSchema.filter((item) => validateTableData(item) !== true);
+ if (faultyRows.length > 0) {
+ errors.push('Please fill out or remove all empty ER-Model-Relationship fields');
+ }
+ return errors;
+};
+
+export function getDatasetName(datainput: any): string {
+ return (
+ datainput?.editableProperties?.name ||
+ datainput?.properties?.name ||
+ datainput?.name ||
+ datainput?.urn.split(',').at(1)
+ );
+}
diff --git a/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/EditableCell.tsx b/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/EditableCell.tsx
new file mode 100644
index 00000000000000..a190d9090cc8c4
--- /dev/null
+++ b/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/EditableCell.tsx
@@ -0,0 +1,75 @@
+import React, { useContext } from 'react';
+import { Form, Select } from 'antd';
+import { EditableContext } from './ERModelRelationUtils';
+import { Dataset } from '../../../../../../types.generated';
+
+interface EditableCellProps {
+ editable: boolean;
+ children: React.ReactNode;
+ dataIndex: keyof ERModelRelationRecord;
+ record: ERModelRelationRecord;
+ tableRecord?: Dataset;
+ value?: any;
+ handleSave: (record: ERModelRelationRecord) => void;
+}
+interface ERModelRelationRecord {
+ key: string;
+ field1Name: string;
+ field2Name: string;
+}
+export const EditableCell = ({
+ editable,
+ children,
+ dataIndex,
+ record,
+ tableRecord,
+ value,
+ handleSave,
+ ...restProps
+}: EditableCellProps) => {
+ const form = useContext(EditableContext)!;
+ const save = async () => {
+ try {
+ const values = await form.validateFields();
+ handleSave({ ...record, ...values });
+ } catch (errInfo) {
+ console.log('Save failed:', errInfo);
+ }
+ };
+
+ let childNode = children;
+ if (editable) {
+ childNode = (
+
+
+ );
+ if (record[dataIndex] !== '') {
+ form.setFieldsValue({ [dataIndex]: record[dataIndex] });
+ }
+ } else {
+ childNode = {children}
;
+ }
+
+ return {childNode} | ;
+};
diff --git a/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/EditableRow.tsx b/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/EditableRow.tsx
new file mode 100644
index 00000000000000..120dff44cfc0d8
--- /dev/null
+++ b/datahub-web-react/src/app/entity/shared/components/styled/ERModelRelationship/EditableRow.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { Form } from 'antd';
+import { EditableContext } from './ERModelRelationUtils';
+
+interface EditableRowProps {
+ index: number;
+}
+export const EditableRow: React.FC = ({ ...propsAt }) => {
+ const [form] = Form.useForm();
+ return (
+
+ );
+};
diff --git a/datahub-web-react/src/app/entity/shared/components/styled/search/EmbeddedListSearchResults.tsx b/datahub-web-react/src/app/entity/shared/components/styled/search/EmbeddedListSearchResults.tsx
index b75f4f10005ffe..26c90edd82b696 100644
--- a/datahub-web-react/src/app/entity/shared/components/styled/search/EmbeddedListSearchResults.tsx
+++ b/datahub-web-react/src/app/entity/shared/components/styled/search/EmbeddedListSearchResults.tsx
@@ -96,6 +96,7 @@ interface Props {
setSelectedEntities: (entities: EntityAndType[]) => any;
numResultsPerPage: number;
setNumResultsPerPage: (numResults: number) => void;
+ singleSelect?: boolean;
entityAction?: React.FC;
applyView?: boolean;
isServerOverloadError?: any;
@@ -120,6 +121,7 @@ export const EmbeddedListSearchResults = ({
setSelectedEntities,
numResultsPerPage,
setNumResultsPerPage,
+ singleSelect,
entityAction,
applyView,
isServerOverloadError,
@@ -181,6 +183,7 @@ export const EmbeddedListSearchResults = ({
selectedEntities={selectedEntities}
setSelectedEntities={setSelectedEntities}
bordered={false}
+ singleSelect={singleSelect}
entityAction={entityAction}
/>
)}
diff --git a/datahub-web-react/src/app/entity/shared/components/styled/search/EntitySearchResults.tsx b/datahub-web-react/src/app/entity/shared/components/styled/search/EntitySearchResults.tsx
index 05bbf01f40cf6c..546fb60e2c3f9b 100644
--- a/datahub-web-react/src/app/entity/shared/components/styled/search/EntitySearchResults.tsx
+++ b/datahub-web-react/src/app/entity/shared/components/styled/search/EntitySearchResults.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Checkbox } from 'antd';
+import { Checkbox, Radio } from 'antd';
import styled from 'styled-components';
import { EntityPath, EntityType, SearchResult } from '../../../../../../types.generated';
import { EntityAndType } from '../../../types';
@@ -9,7 +9,9 @@ import { ListItem, StyledList, ThinDivider } from '../../../../../recommendation
const StyledCheckbox = styled(Checkbox)`
margin-right: 12px;
`;
-
+const StyledRadio = styled(Radio)`
+ margin-right: 12px;
+`;
export type EntityActionProps = {
urn: string;
type: EntityType;
@@ -30,6 +32,7 @@ type Props = {
selectedEntities?: EntityAndType[];
setSelectedEntities?: (entities: EntityAndType[]) => any;
bordered?: boolean;
+ singleSelect?: boolean;
entityAction?: React.FC;
};
@@ -40,6 +43,7 @@ export const EntitySearchResults = ({
selectedEntities = [],
setSelectedEntities,
bordered = true,
+ singleSelect,
entityAction,
}: Props) => {
const entityRegistry = useEntityRegistry();
@@ -60,7 +64,9 @@ export const EntitySearchResults = ({
* Invoked when a new entity is selected. Simply updates the state of the list of selected entities.
*/
const onSelectEntity = (selectedEntity: EntityAndType, selected: boolean) => {
- if (selected) {
+ if (singleSelect && selected) {
+ setSelectedEntities?.([selectedEntity]);
+ } else if (selected) {
setSelectedEntities?.([...selectedEntities, selectedEntity]);
} else {
setSelectedEntities?.(selectedEntities?.filter((entity) => entity.urn !== selectedEntity.urn) || []);
@@ -78,14 +84,30 @@ export const EntitySearchResults = ({
return (
<>
- {isSelectMode && (
- = 0}
- onChange={(e) =>
- onSelectEntity({ urn: entity.urn, type: entity.type }, e.target.checked)
- }
- />
- )}
+ {singleSelect
+ ? isSelectMode && (
+ = 0}
+ onChange={(e) =>
+ onSelectEntity(
+ {
+ urn: entity.urn,
+ type: entity.type,
+ },
+ e.target.checked,
+ )
+ }
+ />
+ )
+ : isSelectMode && (
+ = 0}
+ onChange={(e) =>
+ onSelectEntity({ urn: entity.urn, type: entity.type }, e.target.checked)
+ }
+ />
+ )}
{entityRegistry.renderSearchResult(entity.type, searchResult)}
{entityAction && }
diff --git a/datahub-web-react/src/app/entity/shared/components/styled/search/SearchSelect.tsx b/datahub-web-react/src/app/entity/shared/components/styled/search/SearchSelect.tsx
index 9a92c0edb64192..593a6d804b4af2 100644
--- a/datahub-web-react/src/app/entity/shared/components/styled/search/SearchSelect.tsx
+++ b/datahub-web-react/src/app/entity/shared/components/styled/search/SearchSelect.tsx
@@ -45,6 +45,8 @@ type Props = {
placeholderText?: string | null;
selectedEntities: EntityAndType[];
setSelectedEntities: (Entities: EntityAndType[]) => void;
+ singleSelect?: boolean;
+ hideToolbar?: boolean;
};
/**
@@ -54,7 +56,14 @@ type Props = {
* This component provides easy ways to filter for a specific set of entity types, and provides a set of entity urns
* when the selection is complete.
*/
-export const SearchSelect = ({ fixedEntityTypes, placeholderText, selectedEntities, setSelectedEntities }: Props) => {
+export const SearchSelect = ({
+ fixedEntityTypes,
+ placeholderText,
+ selectedEntities,
+ setSelectedEntities,
+ singleSelect,
+ hideToolbar,
+}: Props) => {
const entityRegistry = useEntityRegistry();
// Component state
@@ -160,16 +169,18 @@ export const SearchSelect = ({ fixedEntityTypes, placeholderText, selectedEntiti
entityRegistry={entityRegistry}
/>
-
- 0 && isListSubset(searchResultUrns, selectedEntityUrns)}
- onChangeSelectAll={onChangeSelectAll}
- showCancel={false}
- showActions={false}
- refetch={refetch}
- selectedEntities={selectedEntities}
- />
-
+ {!hideToolbar && (
+
+ 0 && isListSubset(searchResultUrns, selectedEntityUrns)}
+ onChangeSelectAll={onChangeSelectAll}
+ showCancel={false}
+ showActions={false}
+ refetch={refetch}
+ selectedEntities={selectedEntities}
+ />
+
+ )}
);
diff --git a/datahub-web-react/src/app/entity/shared/components/styled/search/SearchSelectModal.tsx b/datahub-web-react/src/app/entity/shared/components/styled/search/SearchSelectModal.tsx
index b7ee4e72433afc..985be5b3f37c7c 100644
--- a/datahub-web-react/src/app/entity/shared/components/styled/search/SearchSelectModal.tsx
+++ b/datahub-web-react/src/app/entity/shared/components/styled/search/SearchSelectModal.tsx
@@ -21,6 +21,8 @@ type Props = {
continueText?: string | null;
onContinue: (entityUrns: string[]) => void;
onCancel?: () => void;
+ singleSelect?: boolean;
+ hideToolbar?: boolean;
};
/**
@@ -36,6 +38,8 @@ export const SearchSelectModal = ({
continueText,
onContinue,
onCancel,
+ singleSelect,
+ hideToolbar,
}: Props) => {
const [selectedEntities, setSelectedEntities] = useState([]);
@@ -86,6 +90,8 @@ export const SearchSelectModal = ({
placeholderText={placeholderText}
selectedEntities={selectedEntities}
setSelectedEntities={setSelectedEntities}
+ singleSelect={singleSelect}
+ hideToolbar={hideToolbar}
/>
diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/EntityProfile.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/EntityProfile.tsx
index 60d67355d5d7dd..a9737c9698f7b2 100644
--- a/datahub-web-react/src/app/entity/shared/containers/profile/EntityProfile.tsx
+++ b/datahub-web-react/src/app/entity/shared/containers/profile/EntityProfile.tsx
@@ -157,6 +157,8 @@ export const EntityProfile = ({
hideBrowseBar,
subHeader,
}: Props): JSX.Element => {
+ const { config } = useAppConfig();
+ const { erModelRelationshipFeatureEnabled } = config.featureFlags;
const isLineageMode = useIsLineageMode();
const isHideSiblingMode = useIsSeparateSiblingsMode();
const entityRegistry = useEntityRegistry();
@@ -164,6 +166,19 @@ export const EntityProfile = ({
const appConfig = useAppConfig();
const isCompact = React.useContext(CompactContext);
const tabsWithDefaults = tabs.map((tab) => ({ ...tab, display: { ...defaultTabDisplayConfig, ...tab.display } }));
+
+ if (erModelRelationshipFeatureEnabled) {
+ const relationIndex = tabsWithDefaults.findIndex((tab) => {
+ return tab.name === 'Relationships';
+ });
+ if (relationIndex >= 0) {
+ tabsWithDefaults[relationIndex] = {
+ ...tabsWithDefaults[relationIndex],
+ display: { ...defaultTabDisplayConfig },
+ };
+ }
+ }
+
const sortedTabs = sortEntityProfileTabs(appConfig.config, entityType, tabsWithDefaults);
const sideBarSectionsWithDefaults = sidebarSections.map((sidebarSection) => ({
...sidebarSection,
diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Queries/QueryBuilderForm.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Queries/QueryBuilderForm.tsx
index b6780881c6df94..8e65c6b5ad64fd 100644
--- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Queries/QueryBuilderForm.tsx
+++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Queries/QueryBuilderForm.tsx
@@ -77,7 +77,7 @@ export default function QueryBuilderForm({ state, updateState }: Props) {
autoFocus
value={state.title}
onChange={(newTitle) => updateTitle(newTitle.target.value)}
- placeholder="Join Transactions and Users Tables"
+ placeholder="ERModelRelationship Transactions and Users Tables"
/>
Description}>
diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Relationship/RelationshipsTab.less b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Relationship/RelationshipsTab.less
new file mode 100644
index 00000000000000..a78329cb56a41d
--- /dev/null
+++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Relationship/RelationshipsTab.less
@@ -0,0 +1,84 @@
+@import "../../../../../../../node_modules/antd/dist/antd.less";
+
+.RelationshipsTab {
+ .add-btn-link {
+ height: 56px !important;
+ font-family: 'Arial';
+ font-style: normal;
+ font-weight: 700 !important;
+ font-size: 12px !important;
+ color: #262626 !important;
+ }
+ .search-header-div {
+ display: flex;
+ justify-content: space-between;
+ padding: 8px 20px;
+ width: 100% !important;
+ position: absolute;
+ left: 0px;
+ top: 50px;
+ height: 56px !important;
+ background: #FFFFFF;
+ box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.05);
+ }
+ .ant-pagination-item-link svg {
+ margin-bottom: 6px;
+ }
+ .thin-divider{
+ width: 100%;
+ height:0px;
+ border:1px solid rgba(0,0,0,0.12) !important;
+ flex:none;
+ order:5;
+ align-self:stretch;
+ flex-grow:0;
+ }
+}
+.schema-modal {
+ .ant-modal-content {
+ padding: 0px;
+ width: 520px;
+ height: 218px;
+ background: #FFFFFF;
+ border-radius: 5px;
+ }
+ .ant-modal-confirm-title{
+ width: 396px !important;
+ height: 24px !important;
+ font-style: normal;
+ font-weight: 700 !important;
+ font-size: 16px !important;
+ line-height: 24px;
+ color: #262626;
+ margin-top: -8px;
+ }
+ .ant-modal-close-x {
+ padding-top: 26px;
+ padding-left: 18px;
+ }
+ .anticon-exclamation-circle {
+ border-radius: 0px;
+ color: #D63D43;
+ mix-blend-mode: multiply;
+ }
+ .msg-div-inner {
+ width: 472px;
+ font-style: normal;
+ font-weight: 400;
+ font-size: 14px;
+ color: rgba(0, 0, 0, 0.85);
+ margin-left: -27px;
+ padding-top: 18px;
+ }
+ .ant-btn-primary {
+ display: flex;
+ margin-left: 180px;
+ margin-top: -16px;
+ padding: 10px 30px;
+ gap: 8px;
+ width: 84px;
+ height: 40px;
+ background: #1890FF;
+ border-radius: 5px;
+ }
+}
\ No newline at end of file
diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Relationship/RelationshipsTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Relationship/RelationshipsTab.tsx
new file mode 100644
index 00000000000000..269427958ddf92
--- /dev/null
+++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Relationship/RelationshipsTab.tsx
@@ -0,0 +1,273 @@
+import { Button, Card, Divider, Empty, Input, Modal, Pagination } from 'antd';
+import React, { useEffect, useState } from 'react';
+import styled from 'styled-components';
+import { ExclamationCircleFilled, LoadingOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons';
+import { useBaseEntity } from '../../../EntityContext';
+import './RelationshipsTab.less';
+import { EntityType, ErModelRelationship } from '../../../../../../types.generated';
+import { useGetSearchResultsQuery } from '../../../../../../graphql/search.generated';
+import {
+ GetDatasetQuery,
+ useGetDatasetLazyQuery,
+ useGetDatasetSchemaLazyQuery,
+} from '../../../../../../graphql/dataset.generated';
+import { useGetEntityWithSchema } from '../Schema/useGetEntitySchema';
+import closeIcon from '../../../../../../images/close_dark.svg';
+import { CreateERModelRelationModal } from '../../../components/styled/ERModelRelationship/CreateERModelRelationModal';
+import { ERModelRelationPreview } from '../../../components/styled/ERModelRelationship/ERModelRelationPreview';
+import { SearchSelectModal } from '../../../components/styled/search/SearchSelectModal';
+import { ANTD_GRAY } from '../../../constants';
+
+const StyledPagination = styled(Pagination)`
+ margin: 0px;
+ padding: 0px;
+ padding-left: 400px;
+`;
+const StyledInput = styled(Input)`
+ border-radius: 70px;
+ border: 1px solid rgba(0, 0, 0, 0.12);
+ max-width: 416px;
+ height: 40px !important;
+`;
+const ThinDivider = styled(Divider)`
+ height: 1px;
+ width: 520px !important;
+ background: #f0f0f0;
+ margin-left: -70px;
+ margin-bottom: 0px;
+`;
+const NoERModelRelations = styled(Empty)`
+ color: ${ANTD_GRAY[6]};
+ padding-top: 60px;
+`;
+export const RelationshipsTab = () => {
+ const [pageSize, setPageSize] = useState(10);
+ const [currentPage, setCurrentPage] = useState(0);
+ const [filterText, setFilterText] = useState('');
+ const baseEntity = useBaseEntity();
+ // Dynamically load the schema + editable schema information.
+ const { entityWithSchema } = useGetEntityWithSchema();
+ const [modalVisible, setModalVisible] = useState(false);
+ const [ermodelrelationModalVisible, setermodelrelationModalVisible] = useState(false);
+ const tabs = [
+ {
+ key: 'ermodelrelationsTab',
+ tab: 'ER-Model-Relationships',
+ },
+ ];
+ const {
+ data: ermodelrelations,
+ loading: loadingERModelRelation,
+ error: errorERModelRelation,
+ refetch,
+ } = useGetSearchResultsQuery({
+ variables: {
+ input: {
+ type: EntityType.ErModelRelationship,
+ query: `${filterText ? `${filterText}` : ''}`,
+ orFilters: [
+ {
+ and: [
+ {
+ field: 'source',
+ values: [baseEntity?.dataset?.urn || ''],
+ },
+ ],
+ },
+ {
+ and: [
+ {
+ field: 'destination',
+ values: [baseEntity?.dataset?.urn || ''],
+ },
+ ],
+ },
+ ],
+ start: currentPage * pageSize,
+ count: pageSize, // all matched ermodelrelations
+ },
+ },
+ });
+ const totalResults = ermodelrelations?.search?.total || 0;
+ let ermodelrelationData: ErModelRelationship[] = [];
+ if (loadingERModelRelation) {
+ ermodelrelationData = [{}] as ErModelRelationship[];
+ }
+
+ if (!loadingERModelRelation && ermodelrelations?.search && ermodelrelations?.search?.searchResults?.length > 0 && !errorERModelRelation) {
+ ermodelrelationData = ermodelrelations.search.searchResults.map((r) => r.entity as ErModelRelationship);
+ }
+
+ const contentListNoTitle: Record = {
+ ermodelrelationsTab:
+ ermodelrelationData.length > 0 && !loadingERModelRelation ? (
+ ermodelrelationData.map((record) => {
+ return (
+ <>
+
+ >
+ );
+ })
+ ) : (
+ <>
+ {!loadingERModelRelation && (
+
+
+
+ )}
+ {loadingERModelRelation && (
+
+ ER-Model-Relationships
+
+ )}
+ >
+ ),
+ };
+ const [activeTabKey, setActiveTabKey] = useState('ermodelrelationsTab');
+ const onTabChange = (key: string) => {
+ setActiveTabKey(key);
+ };
+ const [table2LazySchema, setTable2LazySchema] = useState(undefined as any);
+ const [getTable2LazySchema] = useGetDatasetSchemaLazyQuery({
+ onCompleted: (data) => {
+ setTable2LazySchema(data);
+ },
+ });
+ const [table2LazyDataset, setTable2LazyDataset] = useState(undefined as any);
+ const [getTable2LazyDataset] = useGetDatasetLazyQuery({
+ onCompleted: (data) => {
+ setTable2LazyDataset(data);
+ },
+ });
+
+ const schemaIssueModal = () => {
+ Modal.error({
+ title: `Schema error`,
+ className: 'schema-modal',
+ content: (
+
+
+
+ A schema was not ingested for the dataset selected. ERModelRelation cannot be created.
+
+
+
+ ),
+ onOk() {},
+ okText: 'Ok',
+ icon: ,
+ closeIcon: ,
+ maskClosable: true,
+ closable: true,
+ });
+ };
+ useEffect(() => {
+ if (
+ table2LazySchema?.dataset !== undefined &&
+ table2LazySchema?.dataset?.schemaMetadata?.fields === undefined
+ ) {
+ schemaIssueModal();
+ }
+ if (
+ table2LazySchema?.dataset !== undefined &&
+ table2LazySchema?.dataset?.schemaMetadata?.fields !== undefined
+ ) {
+ setermodelrelationModalVisible(false);
+ setModalVisible(true);
+ }
+ }, [table2LazySchema]);
+ return (
+ <>
+ {ermodelrelationModalVisible && (
+ {
+ await getTable2LazySchema({
+ variables: {
+ urn: selectedDataSet[0] || '',
+ },
+ });
+ await getTable2LazyDataset({
+ variables: {
+ urn: selectedDataSet[0] || '',
+ },
+ });
+ }}
+ onCancel={() => setermodelrelationModalVisible(false)}
+ fixedEntityTypes={[EntityType.Dataset]}
+ singleSelect
+ hideToolbar
+ />
+ )}
+ {baseEntity !== undefined && (
+ {
+ setModalVisible(false);
+ }}
+ refetch={refetch}
+ />
+ )}
+ {
+ onTabChange(key);
+ }}
+ >
+
+
setFilterText(e.target.value)}
+ allowClear
+ autoFocus
+ prefix={}
+ />
+
+ {' '}
+
+
+ {contentListNoTitle[activeTabKey]}
+ {totalResults >= 1 && (
+ setCurrentPage(page - 1)}
+ showSizeChanger={totalResults > 10}
+ onShowSizeChange={(_currNum, newNum) => setPageSize(newNum)}
+ pageSizeOptions={['10', '20', '50', '100']}
+ />
+ )}
+
+ >
+ );
+};
diff --git a/datahub-web-react/src/app/entity/shared/tabs/ERModelRelationship/ERModelRelationshipTab.less b/datahub-web-react/src/app/entity/shared/tabs/ERModelRelationship/ERModelRelationshipTab.less
new file mode 100644
index 00000000000000..bb8f2f04f6a37c
--- /dev/null
+++ b/datahub-web-react/src/app/entity/shared/tabs/ERModelRelationship/ERModelRelationshipTab.less
@@ -0,0 +1,44 @@
+@import "../../../../../../node_modules/antd/dist/antd.less";
+
+.ERModelRelationTab {
+ .add-btn-link {
+ padding-left: 1155px !important;
+ padding-top: 16px !important;
+ width: 50px;
+ height: 20px !important;
+ font-family: 'Arial';
+ font-style: normal;
+ font-weight: 700 !important;
+ font-size: 12px !important;
+ line-height: 20px !important;
+ color: #262626 !important;
+
+ }
+ .thin-divider{
+ width: 100%;
+ height:0px;
+ border:1px solid rgba(0,0,0,0.12) !important;
+ flex:none;
+ order:5;
+ align-self:stretch;
+ flex-grow:0;
+ }
+ .search-header-div {
+
+ padding: 8px 20px;
+ gap: 238px;
+
+ position: absolute;
+ left: 0px;
+ top: 50px;
+
+ width: 1292px !important;
+ height: 56px !important;
+ background: #FFFFFF;
+ box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.05);
+ }
+ .ermodelrelation-preview-div {
+ padding-left: 22px;
+ padding-top: 34px;
+ }
+}
\ No newline at end of file
diff --git a/datahub-web-react/src/app/entity/shared/tabs/ERModelRelationship/ERModelRelationshipTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/ERModelRelationship/ERModelRelationshipTab.tsx
new file mode 100644
index 00000000000000..946bf429d3e87c
--- /dev/null
+++ b/datahub-web-react/src/app/entity/shared/tabs/ERModelRelationship/ERModelRelationshipTab.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { Divider } from 'antd';
+import './ERModelRelationshipTab.less';
+import { ERModelRelationPreview } from '../../components/styled/ERModelRelationship/ERModelRelationPreview';
+import { useEntityData, useRefetch } from '../../EntityContext';
+
+export const ERModelRelationshipTab = () => {
+ const { entityData } = useEntityData();
+ const refetch = useRefetch();
+ const ermodelrelationView = (ermodelrelationData?: any): JSX.Element => {
+ return ;
+ };
+ return (
+ <>
+
+
{ermodelrelationView(entityData)}
+
+
+ >
+ );
+};
diff --git a/datahub-web-react/src/appConfigContext.tsx b/datahub-web-react/src/appConfigContext.tsx
index 356b267cbcf279..00feaf82234100 100644
--- a/datahub-web-react/src/appConfigContext.tsx
+++ b/datahub-web-react/src/appConfigContext.tsx
@@ -48,6 +48,7 @@ export const DEFAULT_APP_CONFIG = {
showSearchFiltersV2: true,
showBrowseV2: true,
showAcrylInfo: false,
+ erModelRelationshipFeatureEnabled: false,
showAccessManagement: false,
nestedDomainsEnabled: true,
platformBrowseV2: false,
diff --git a/datahub-web-react/src/graphql/app.graphql b/datahub-web-react/src/graphql/app.graphql
index fe283403491479..b7527d53b5705f 100644
--- a/datahub-web-react/src/graphql/app.graphql
+++ b/datahub-web-react/src/graphql/app.graphql
@@ -63,6 +63,7 @@ query appConfig {
showSearchFiltersV2
showBrowseV2
showAcrylInfo
+ erModelRelationshipFeatureEnabled
showAccessManagement
nestedDomainsEnabled
platformBrowseV2
diff --git a/datahub-web-react/src/graphql/ermodelrelationship.graphql b/datahub-web-react/src/graphql/ermodelrelationship.graphql
new file mode 100644
index 00000000000000..c3163c7c21ffe1
--- /dev/null
+++ b/datahub-web-react/src/graphql/ermodelrelationship.graphql
@@ -0,0 +1,57 @@
+query getERModelRelationship($urn: String!) {
+ erModelRelationship(urn: $urn) {
+ urn
+ type
+ id
+ properties {
+ ...ermodelrelationPropertiesFields
+ }
+ editableProperties {
+ ...ermodelrelationEditablePropertiesFields
+ }
+ institutionalMemory {
+ ...institutionalMemoryFields
+ }
+ ownership {
+ ...ownershipFields
+ }
+ status {
+ removed
+ }
+ tags {
+ ...globalTagsFields
+ }
+ glossaryTerms {
+ ...glossaryTerms
+ }
+ outgoing: relationships(
+ input: { types: ["ermodelrelationA", "ermodelrelationB"], direction: OUTGOING, start: 0, count: 100 }
+ ) {
+ start
+ count
+ total
+ relationships {
+ type
+ direction
+ entity {
+ ... on ERModelRelationship {
+ urn
+ }
+
+ }
+ }
+ }
+ }
+}
+
+mutation updateERModelRelationship($urn: String!, $input: ERModelRelationshipUpdateInput!) {
+ updateERModelRelationship(urn: $urn, input: $input)
+}
+
+mutation createERModelRelationship($input: ERModelRelationshipUpdateInput!) {
+ createERModelRelationship(input: $input) {
+ urn
+ type
+ id
+ }
+}
\ No newline at end of file
diff --git a/datahub-web-react/src/graphql/fragments.graphql b/datahub-web-react/src/graphql/fragments.graphql
index 9ee2a8615be99b..a13f9162602fc1 100644
--- a/datahub-web-react/src/graphql/fragments.graphql
+++ b/datahub-web-react/src/graphql/fragments.graphql
@@ -1186,6 +1186,47 @@ fragment entityDisplayNameFields on Entity {
}
}
+fragment ermodelrelationPropertiesFields on ERModelRelationshipProperties {
+ name
+ source {
+ ...datasetERModelRelationshipFields
+ }
+ destination {
+ ...datasetERModelRelationshipFields
+ }
+ relationshipFieldMappings {
+ ...relationshipFieldMapping
+ }
+ createdTime
+ createdActor {
+ urn
+ }
+}
+
+fragment relationshipFieldMapping on RelationshipFieldMapping {
+ sourceField
+ destinationField
+}
+
+fragment ermodelrelationEditablePropertiesFields on ERModelRelationshipEditableProperties {
+ description
+ name
+}
+
+fragment datasetERModelRelationshipFields on Dataset {
+ urn
+ name
+ properties {
+ name
+ description
+ }
+ editableProperties {
+ description
+ }
+ schemaMetadata {
+ ...schemaMetadataFields
+ }
+}
fragment structuredPropertyFields on StructuredPropertyEntity {
urn
type
diff --git a/datahub-web-react/src/graphql/lineage.graphql b/datahub-web-react/src/graphql/lineage.graphql
index b73a99488cf8af..612adcc5f777d0 100644
--- a/datahub-web-react/src/graphql/lineage.graphql
+++ b/datahub-web-react/src/graphql/lineage.graphql
@@ -269,6 +269,26 @@ fragment lineageNodeProperties on EntityWithRelationships {
... on MLPrimaryKey {
...nonRecursiveMLPrimaryKey
}
+ ... on ERModelRelationship {
+ urn
+ type
+ id
+ properties {
+ ...ermodelrelationPropertiesFields
+ }
+ editableProperties {
+ ...ermodelrelationEditablePropertiesFields
+ }
+ ownership {
+ ...ownershipFields
+ }
+ tags {
+ ...globalTagsFields
+ }
+ glossaryTerms {
+ ...glossaryTerms
+ }
+ }
... on Restricted {
urn
type
diff --git a/datahub-web-react/src/graphql/relationships.graphql b/datahub-web-react/src/graphql/relationships.graphql
index e2afff5dfa6d27..7d6dd28dfe0299 100644
--- a/datahub-web-react/src/graphql/relationships.graphql
+++ b/datahub-web-react/src/graphql/relationships.graphql
@@ -42,4 +42,4 @@ fragment relationshipFields on EntityWithRelationships {
downstream: lineage(input: { direction: DOWNSTREAM, start: 0, count: 100 }) {
...leafLineageResults
}
-}
+}
\ No newline at end of file
diff --git a/datahub-web-react/src/graphql/search.graphql b/datahub-web-react/src/graphql/search.graphql
index e8694447a0657a..e9cca5e64dc66d 100644
--- a/datahub-web-react/src/graphql/search.graphql
+++ b/datahub-web-react/src/graphql/search.graphql
@@ -825,6 +825,26 @@ fragment searchResultFields on Entity {
... on DataProduct {
...dataProductSearchFields
}
+ ... on ERModelRelationship {
+ urn
+ type
+ id
+ properties {
+ ...ermodelrelationPropertiesFields
+ }
+ editableProperties {
+ ...ermodelrelationEditablePropertiesFields
+ }
+ ownership {
+ ...ownershipFields
+ }
+ tags {
+ ...globalTagsFields
+ }
+ glossaryTerms {
+ ...glossaryTerms
+ }
+ }
}
fragment facetFields on FacetMetadata {
diff --git a/datahub-web-react/src/images/Arrow.svg b/datahub-web-react/src/images/Arrow.svg
new file mode 100644
index 00000000000000..c918392e324bb9
--- /dev/null
+++ b/datahub-web-react/src/images/Arrow.svg
@@ -0,0 +1,3 @@
+
diff --git a/datahub-web-react/src/images/close_dark.svg b/datahub-web-react/src/images/close_dark.svg
new file mode 100644
index 00000000000000..4ef90fa3f6cccd
--- /dev/null
+++ b/datahub-web-react/src/images/close_dark.svg
@@ -0,0 +1,3 @@
+
diff --git a/datahub-web-react/src/images/editIconBlack.svg b/datahub-web-react/src/images/editIconBlack.svg
new file mode 100644
index 00000000000000..15478b2536add4
--- /dev/null
+++ b/datahub-web-react/src/images/editIconBlack.svg
@@ -0,0 +1,3 @@
+
diff --git a/datahub-web-react/src/images/ermodelrelationshipIcon.svg b/datahub-web-react/src/images/ermodelrelationshipIcon.svg
new file mode 100644
index 00000000000000..06046590a62a5a
--- /dev/null
+++ b/datahub-web-react/src/images/ermodelrelationshipIcon.svg
@@ -0,0 +1,3 @@
+
diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml
index 23ac821670e44b..c7a3c5098d940d 100644
--- a/docker/docker-compose.dev.yml
+++ b/docker/docker-compose.dev.yml
@@ -11,7 +11,7 @@
version: '3.9'
services:
datahub-frontend-react:
- image: linkedin/datahub-frontend-react:debug
+ image: linkedin/datahub-frontend-react:head
ports:
- ${DATAHUB_MAPPED_FRONTEND_DEBUG_PORT:-5002}:5002
- ${DATAHUB_MAPPED_FRONTEND_PORT:-9002}:9002
@@ -46,6 +46,7 @@ services:
- SEARCH_SERVICE_ENABLE_CACHE=false
- LINEAGE_SEARCH_CACHE_ENABLED=false
- SHOW_BROWSE_V2=true
+ - ER_MODEL_RELATIONSHIP_FEATURE_ENABLED=false
- KAFKA_CONSUMER_STOP_ON_DESERIALIZATION_ERROR=${KAFKA_CONSUMER_STOP_ON_DESERIALIZATION_ERROR:-true}
volumes:
- ./datahub-gms/start.sh:/datahub/datahub-gms/scripts/start.sh
@@ -75,7 +76,7 @@ services:
- ${HOME}/.datahub/plugins:/etc/datahub/plugins
# Pre-creates the search indices using local mapping/settings.json
elasticsearch-setup:
- image: linkedin/datahub-elasticsearch-setup:debug
+ image: linkedin/datahub-elasticsearch-setup:head
build:
context: elasticsearch-setup
dockerfile: Dockerfile
diff --git a/docs/deploy/environment-vars.md b/docs/deploy/environment-vars.md
index 4c7b249349ca01..e2354e398ecb97 100644
--- a/docs/deploy/environment-vars.md
+++ b/docs/deploy/environment-vars.md
@@ -12,8 +12,10 @@ DataHub works.
| Variable | Default | Unit/Type | Components | Description |
|--------------------------------------------------|---------|-----------|-----------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
| `UI_INGESTION_ENABLED` | `true` | boolean | [`GMS`, `MCE Consumer`] | Enable UI based ingestion. |
-| `DATAHUB_ANALYTICS_ENABLED` | `true` | boolean | [`Frontend`, `GMS`] | Collect DataHub usage to populate the analytics dashboard. | |
+| `DATAHUB_ANALYTICS_ENABLED` | `true` | boolean | [`Frontend`, `GMS`] | Collect DataHub usage to populate the analytics dashboard. |
| `BOOTSTRAP_SYSTEM_UPDATE_WAIT_FOR_SYSTEM_UPDATE` | `true` | boolean | [`GMS`, `MCE Consumer`, `MAE Consumer`] | Do not wait for the `system-update` to complete before starting. This should typically only be disabled during development. |
+| `ER_MODEL_RELATIONSHIP_FEATURE_ENABLED` | `false` | boolean | [`Frontend`, `GMS`] | Enable ER Model Relation Feature that shows Relationships Tab within a Dataset UI. |
+
## Ingestion
@@ -89,4 +91,4 @@ Simply replace the dot, `.`, with an underscore, `_`, and convert to uppercase.
| `AUTH_OIDC_GROUPS_CLAIM` | `groups` | string | [`Frontend`] | Claim to use as the user's group. |
| `AUTH_OIDC_EXTRACT_GROUPS_ENABLED` | `false` | boolean | [`Frontend`] | Auto-provision the group from the user's group claim. |
| `AUTH_SESSION_TTL_HOURS` | `24` | string | [`Frontend`] | The number of hours a user session is valid. After this many hours the actor cookie will be expired by the browser and the user will be prompted to login again. |
-| `MAX_SESSION_TOKEN_AGE` | `24h` | string | [`Frontend`] | The maximum age of the session token. [User session tokens are stateless and will become invalid after this time](https://www.playframework.com/documentation/2.8.x/SettingsSession#Session-Timeout-/-Expiration) requiring a user to login again. |
\ No newline at end of file
+| `MAX_SESSION_TOKEN_AGE` | `24h` | string | [`Frontend`] | The maximum age of the session token. [User session tokens are stateless and will become invalid after this time](https://www.playframework.com/documentation/2.8.x/SettingsSession#Session-Timeout-/-Expiration) requiring a user to login again. |
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 6e089865956617..62490c3c567a97 100644
--- a/li-utils/src/main/java/com/linkedin/metadata/Constants.java
+++ b/li-utils/src/main/java/com/linkedin/metadata/Constants.java
@@ -62,6 +62,7 @@ public class Constants {
public static final String TAG_ENTITY_NAME = "tag";
public static final String CONTAINER_ENTITY_NAME = "container";
public static final String DOMAIN_ENTITY_NAME = "domain";
+ public static final String ER_MODEL_RELATIONSHIP_ENTITY_NAME = "erModelRelationship";
public static final String ASSERTION_ENTITY_NAME = "assertion";
public static final String INCIDENT_ENTITY_NAME = "incident";
public static final String INGESTION_SOURCE_ENTITY_NAME = "dataHubIngestionSource";
@@ -268,6 +269,13 @@ public class Constants {
public static final String DOMAIN_CREATED_TIME_INDEX_FIELD_NAME = "createdTime";
+ // ERModelRelationship
+ public static final String ER_MODEL_RELATIONSHIP_KEY_ASPECT_NAME = "erModelRelationshipKey";
+ public static final String ER_MODEL_RELATIONSHIP_PROPERTIES_ASPECT_NAME =
+ "erModelRelationshipProperties";
+ public static final String EDITABLE_ER_MODEL_RELATIONSHIP_PROPERTIES_ASPECT_NAME =
+ "editableERModelRelationshipProperties";
+
// Assertion
public static final String ASSERTION_KEY_ASPECT_NAME = "assertionKey";
public static final String ASSERTION_INFO_ASPECT_NAME = "assertionInfo";
diff --git a/li-utils/src/main/javaPegasus/com/linkedin/common/urn/ERModelRelationshipUrn.java b/li-utils/src/main/javaPegasus/com/linkedin/common/urn/ERModelRelationshipUrn.java
new file mode 100644
index 00000000000000..f6313bd048d31e
--- /dev/null
+++ b/li-utils/src/main/javaPegasus/com/linkedin/common/urn/ERModelRelationshipUrn.java
@@ -0,0 +1,71 @@
+package com.linkedin.common.urn;
+
+import com.linkedin.data.template.Custom;
+import com.linkedin.data.template.DirectCoercer;
+import com.linkedin.data.template.TemplateOutputCastException;
+import java.net.URISyntaxException;
+
+public class ERModelRelationshipUrn extends Urn {
+ public static final String ENTITY_TYPE = "erModelRelationship";
+
+ private final String _ermodelrelationId;
+
+ public ERModelRelationshipUrn(String ermodelrelationId) {
+ super(ENTITY_TYPE, TupleKey.create(ermodelrelationId));
+ this._ermodelrelationId = ermodelrelationId;
+ }
+
+ public String getERModelRelationIdEntity() {
+ return _ermodelrelationId;
+ }
+
+ public static ERModelRelationshipUrn createFromString(String rawUrn) throws URISyntaxException {
+ return createFromUrn(Urn.createFromString(rawUrn));
+ }
+
+ public static ERModelRelationshipUrn createFromUrn(Urn urn) throws URISyntaxException {
+ if (!"li".equals(urn.getNamespace())) {
+ throw new URISyntaxException(urn.toString(), "Urn namespace type should be 'li'.");
+ } else if (!ENTITY_TYPE.equals(urn.getEntityType())) {
+ throw new URISyntaxException(
+ urn.toString(), "Urn entity type should be 'erModelRelationship'.");
+ } else {
+ TupleKey key = urn.getEntityKey();
+ if (key.size() != 1) {
+ throw new URISyntaxException(urn.toString(), "Invalid number of keys.");
+ } else {
+ try {
+ return new ERModelRelationshipUrn((String) key.getAs(0, String.class));
+ } catch (Exception var3) {
+ throw new URISyntaxException(
+ urn.toString(), "Invalid URN Parameter: '" + var3.getMessage());
+ }
+ }
+ }
+ }
+
+ public static ERModelRelationshipUrn deserialize(String rawUrn) throws URISyntaxException {
+ return createFromString(rawUrn);
+ }
+
+ static {
+ Custom.initializeCustomClass(ERModelRelationshipUrn.class);
+ Custom.registerCoercer(
+ new DirectCoercer() {
+ public Object coerceInput(ERModelRelationshipUrn object) throws ClassCastException {
+ return object.toString();
+ }
+
+ public ERModelRelationshipUrn coerceOutput(Object object)
+ throws TemplateOutputCastException {
+ try {
+ return com.linkedin.common.urn.ERModelRelationshipUrn.createFromString(
+ (String) object);
+ } catch (URISyntaxException e) {
+ throw new TemplateOutputCastException("Invalid URN syntax: " + e.getMessage(), e);
+ }
+ }
+ },
+ ERModelRelationshipUrn.class);
+ }
+}
diff --git a/li-utils/src/main/pegasus/com/linkedin/common/ERModelRelationshipUrn.pdl b/li-utils/src/main/pegasus/com/linkedin/common/ERModelRelationshipUrn.pdl
new file mode 100644
index 00000000000000..83690cc9d3e81e
--- /dev/null
+++ b/li-utils/src/main/pegasus/com/linkedin/common/ERModelRelationshipUrn.pdl
@@ -0,0 +1,24 @@
+namespace com.linkedin.common
+
+/**
+ * Standardized erModelRelationship identifier.
+ */
+@java.class = "com.linkedin.common.urn.ERModelRelationshipUrn"
+@validate.`com.linkedin.common.validator.TypedUrnValidator` = {
+ "accessible" : true,
+ "owningTeam" : "urn:li:internalTeam:datahub",
+ "entityType" : "erModelRelationship",
+ "constructable" : true,
+ "namespace" : "li",
+ "name" : "ERModelRelationship",
+ "doc" : "Standardized ERModelRelationship identifier.",
+ "owners" : [ "urn:li:corpuser:fbar", "urn:li:corpuser:bfoo" ],
+ "fields" : [ {
+ "name" : "id",
+ "doc" : "ERModelRelationship native name e.g. ., /dir/subdir/, or ",
+ "type" : "string",
+ "maxLength" : 284
+ }],
+ "maxLength" : 284
+}
+typeref ERModelRelationshipUrn = string
\ No newline at end of file
diff --git a/metadata-models/src/main/pegasus/com/linkedin/ermodelrelation/ERModelRelationshipProperties.pdl b/metadata-models/src/main/pegasus/com/linkedin/ermodelrelation/ERModelRelationshipProperties.pdl
new file mode 100644
index 00000000000000..6fbfcbd97246e7
--- /dev/null
+++ b/metadata-models/src/main/pegasus/com/linkedin/ermodelrelation/ERModelRelationshipProperties.pdl
@@ -0,0 +1,90 @@
+namespace com.linkedin.ermodelrelation
+
+import com.linkedin.common.AuditStamp
+import com.linkedin.common.Urn
+import com.linkedin.common.CustomProperties
+
+/**
+ * Properties associated with a ERModelRelationship
+ */
+@Aspect = {
+ "name": "erModelRelationshipProperties"
+}
+record ERModelRelationshipProperties includes CustomProperties {
+
+ /**
+ * Name of the ERModelRelation
+ */
+ @Searchable = {
+ "fieldType": "TEXT_PARTIAL",
+ "enableAutocomplete": true,
+ "boostScore": 10.0
+ }
+ name: string
+
+ /**
+ * First dataset in the erModelRelationship (no directionality)
+ */
+ @Relationship = {
+ "name": "ermodelrelationA",
+ "entityTypes": [ "dataset" ]
+ }
+ @Searchable = {
+ "fieldType": "TEXT_PARTIAL",
+ "enableAutocomplete": true,
+ "boostScore": 10.0
+ }
+ source: Urn
+
+ /**
+ * Second dataset in the erModelRelationship (no directionality)
+ */
+ @Relationship = {
+ "name": "ermodelrelationB",
+ "entityTypes": [ "dataset" ]
+ }
+ @Searchable = {
+ "fieldType": "TEXT_PARTIAL",
+ "enableAutocomplete": true,
+ "boostScore": 10.0
+ }
+ destination: Urn
+
+
+ /**
+ * ERModelRelationFieldMapping (in future we can make it an array)
+ */
+ relationshipFieldMappings: array [RelationshipFieldMapping]
+
+ /**
+ * A timestamp documenting when the asset was created in the source Data Platform (not on DataHub)
+ */
+ @Searchable = {
+ "/time": {
+ "fieldName": "createdAt",
+ "fieldType": "DATETIME"
+ }
+ }
+ created: optional AuditStamp
+
+ /**
+ * A timestamp documenting when the asset was last modified in the source Data Platform (not on DataHub)
+ */
+ @Searchable = {
+ "/time": {
+ "fieldName": "lastModifiedAt",
+ "fieldType": "DATETIME"
+ }
+ }
+ lastModified: optional AuditStamp
+
+ /**
+ * Cardinality of the relationship
+ */
+ cardinality: enum ERModelRelationshipCardinality {
+ ONE_ONE
+ ONE_N
+ N_ONE
+ N_N
+ } = "N_N"
+}
\ No newline at end of file
diff --git a/metadata-models/src/main/pegasus/com/linkedin/ermodelrelation/EditableERModelRelationshipProperties.pdl b/metadata-models/src/main/pegasus/com/linkedin/ermodelrelation/EditableERModelRelationshipProperties.pdl
new file mode 100644
index 00000000000000..862c18f828841b
--- /dev/null
+++ b/metadata-models/src/main/pegasus/com/linkedin/ermodelrelation/EditableERModelRelationshipProperties.pdl
@@ -0,0 +1,31 @@
+namespace com.linkedin.ermodelrelation
+
+import com.linkedin.common.ChangeAuditStamps
+
+
+/**
+ * EditableERModelRelationProperties stores editable changes made to erModelRelationship properties. This separates changes made from
+ * ingestion pipelines and edits in the UI to avoid accidental overwrites of user-provided data by ingestion pipelines
+ */
+@Aspect = {
+ "name": "editableERModelRelationshipProperties"
+}
+record EditableERModelRelationshipProperties includes ChangeAuditStamps {
+ /**
+ * Documentation of the erModelRelationship
+ */
+ @Searchable = {
+ "fieldType": "TEXT",
+ "fieldName": "editedDescription",
+ }
+ description: optional string
+
+ /**
+ * Display name of the ERModelRelation
+ */
+ @Searchable = {
+ "fieldType": "TEXT_PARTIAL",
+ "fieldName": "editedName",
+ }
+ name: optional string
+}
diff --git a/metadata-models/src/main/pegasus/com/linkedin/ermodelrelation/RelationshipFieldMapping.pdl b/metadata-models/src/main/pegasus/com/linkedin/ermodelrelation/RelationshipFieldMapping.pdl
new file mode 100644
index 00000000000000..48df4d6a1c5ee3
--- /dev/null
+++ b/metadata-models/src/main/pegasus/com/linkedin/ermodelrelation/RelationshipFieldMapping.pdl
@@ -0,0 +1,18 @@
+namespace com.linkedin.ermodelrelation
+
+import com.linkedin.dataset.SchemaFieldPath
+
+/**
+ * Individual Field Mapping of a relationship- one of several
+ */
+record RelationshipFieldMapping {
+ /**
+ * All fields from dataset A that are required for the join, maps to bFields 1:1
+ */
+ sourceField: SchemaFieldPath
+
+ /**
+ * All fields from dataset B that are required for the join, maps to aFields 1:1
+ */
+ destinationField: SchemaFieldPath
+}
\ No newline at end of file
diff --git a/metadata-models/src/main/pegasus/com/linkedin/metadata/key/ERModelRelationshipKey.pdl b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/ERModelRelationshipKey.pdl
new file mode 100644
index 00000000000000..ba87b22b4dfd39
--- /dev/null
+++ b/metadata-models/src/main/pegasus/com/linkedin/metadata/key/ERModelRelationshipKey.pdl
@@ -0,0 +1,14 @@
+namespace com.linkedin.metadata.key
+
+/**
+ * Key for a ERModelRelationship
+ */
+@Aspect = {
+ "name": "erModelRelationshipKey"
+}
+record ERModelRelationshipKey {
+ /*
+ * Unique guid for ERModelRelationship
+ */
+ id: string
+}
\ No newline at end of file
diff --git a/metadata-models/src/main/resources/entity-registry.yml b/metadata-models/src/main/resources/entity-registry.yml
index 22fe6551eb5285..b335c66cc0bb79 100644
--- a/metadata-models/src/main/resources/entity-registry.yml
+++ b/metadata-models/src/main/resources/entity-registry.yml
@@ -482,6 +482,17 @@ entities:
keyAspect: dataHubViewKey
aspects:
- dataHubViewInfo
+ - name: erModelRelationship
+ doc: ER Model Relationship of Dataset Fields
+ keyAspect: erModelRelationshipKey
+ aspects:
+ - erModelRelationshipProperties
+ - editableERModelRelationshipProperties
+ - institutionalMemory
+ - ownership
+ - status
+ - globalTags
+ - glossaryTerms
- name: query
category: core
keyAspect: queryKey
diff --git a/metadata-service/configuration/src/main/resources/application.yml b/metadata-service/configuration/src/main/resources/application.yml
index c0f82d85369220..8eaf1ed294c6ab 100644
--- a/metadata-service/configuration/src/main/resources/application.yml
+++ b/metadata-service/configuration/src/main/resources/application.yml
@@ -364,6 +364,7 @@ featureFlags:
uiEnabled: ${PRE_PROCESS_HOOKS_UI_ENABLED:true} # Circumvents Kafka for processing index updates for UI changes sourced from GraphQL to avoid processing delays
reprocessEnabled: ${PRE_PROCESS_HOOKS_UI_ENABLED:false} # If enabled, will reprocess UI sourced events asynchronously when reading from Kafka after pre-processing them synchronously
showAcrylInfo: ${SHOW_ACRYL_INFO:false} # Show different CTAs within DataHub around moving to Managed DataHub. Set to true for the demo site.
+ erModelRelationshipFeatureEnabled: ${ER_MODEL_RELATIONSHIP_FEATURE_ENABLED:false} # Enable Join Tables Feature and show within Dataset view as Relations
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
diff --git a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/ermodelrelation/ERModelRelationshipServiceFactory.java b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/ermodelrelation/ERModelRelationshipServiceFactory.java
new file mode 100644
index 00000000000000..36c03f4ba32c9c
--- /dev/null
+++ b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/ermodelrelation/ERModelRelationshipServiceFactory.java
@@ -0,0 +1,30 @@
+package com.linkedin.gms.factory.ermodelrelation;
+
+import com.datahub.authentication.Authentication;
+import com.linkedin.entity.client.EntityClient;
+import com.linkedin.metadata.service.ERModelRelationshipService;
+import com.linkedin.metadata.spring.YamlPropertySourceFactory;
+import javax.annotation.Nonnull;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.context.annotation.Scope;
+
+@Configuration
+@PropertySource(value = "classpath:/application.yml", factory = YamlPropertySourceFactory.class)
+public class ERModelRelationshipServiceFactory {
+
+ @Autowired
+ @Qualifier("systemAuthentication")
+ private Authentication _authentication;
+
+ @Bean(name = "erModelRelationshipService")
+ @Scope("singleton")
+ @Nonnull
+ protected ERModelRelationshipService getInstance(
+ @Qualifier("entityClient") final EntityClient entityClient) throws Exception {
+ return new ERModelRelationshipService(entityClient, _authentication);
+ }
+}
diff --git a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/graphql/GraphQLEngineFactory.java b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/graphql/GraphQLEngineFactory.java
index 73fb026398d2d2..5a4d22428c29bc 100644
--- a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/graphql/GraphQLEngineFactory.java
+++ b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/graphql/GraphQLEngineFactory.java
@@ -28,6 +28,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;
@@ -160,6 +161,10 @@ public class GraphQLEngineFactory {
@Qualifier("queryService")
private QueryService queryService;
+ @Autowired
+ @Qualifier("erModelRelationshipService")
+ private ERModelRelationshipService erModelRelationshipService;
+
@Autowired
@Qualifier("dataProductService")
private DataProductService dataProductService;
@@ -216,6 +221,7 @@ protected GraphQLEngine graphQLEngine(
args.setSettingsService(settingsService);
args.setLineageService(lineageService);
args.setQueryService(queryService);
+ args.setErModelRelationshipService(erModelRelationshipService);
args.setFeatureFlags(configProvider.getFeatureFlags());
args.setFormService(formService);
args.setRestrictedService(restrictedService);
diff --git a/metadata-service/services/src/main/java/com/linkedin/metadata/service/ERModelRelationshipService.java b/metadata-service/services/src/main/java/com/linkedin/metadata/service/ERModelRelationshipService.java
new file mode 100644
index 00000000000000..ca0f2f2db56a82
--- /dev/null
+++ b/metadata-service/services/src/main/java/com/linkedin/metadata/service/ERModelRelationshipService.java
@@ -0,0 +1,68 @@
+package com.linkedin.metadata.service;
+
+import static com.linkedin.metadata.Constants.*;
+
+import com.datahub.authentication.Authentication;
+import com.google.common.collect.ImmutableSet;
+import com.linkedin.common.urn.Urn;
+import com.linkedin.entity.EntityResponse;
+import com.linkedin.entity.client.EntityClient;
+import com.linkedin.metadata.Constants;
+import java.util.Objects;
+import java.util.Set;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * This class is used to permit easy CRUD operations on a Query. Currently it supports creating and
+ * removing a Query.
+ *
+ * Note that no Authorization is performed within the service. The expectation is that the caller
+ * has already verified the permissions of the active Actor.
+ */
+@Slf4j
+public class ERModelRelationshipService extends BaseService {
+
+ public ERModelRelationshipService(
+ @Nonnull EntityClient entityClient, @Nonnull Authentication systemAuthentication) {
+ super(entityClient, systemAuthentication);
+ }
+
+ 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);
+
+ /**
+ * Returns an instance of {@link EntityResponse} for the specified ERModelRelationship urn, or
+ * null if one cannot be found.
+ *
+ * @param ermodelrelationUrn the urn of the Query
+ * @param authentication the authentication to use
+ * @return an instance of {@link EntityResponse} for the ERModelRelationship, null if it does not
+ * exist.
+ */
+ @Nullable
+ public EntityResponse getERModelRelationshipResponse(
+ @Nonnull final Urn ermodelrelationUrn, @Nonnull final Authentication authentication) {
+ Objects.requireNonNull(ermodelrelationUrn, "ermodelrelationUrn must not be null");
+ Objects.requireNonNull(authentication, "authentication must not be null");
+ try {
+ return this.entityClient.getV2(
+ Constants.ER_MODEL_RELATIONSHIP_ENTITY_NAME,
+ ermodelrelationUrn,
+ ASPECTS_TO_RESOLVE,
+ authentication);
+ } catch (Exception e) {
+ throw new RuntimeException(
+ String.format("Failed to retrieve Query with urn %s", ermodelrelationUrn), e);
+ }
+ }
+}
diff --git a/metadata-service/war/src/main/resources/boot/policies.json b/metadata-service/war/src/main/resources/boot/policies.json
index b89ee970c875f3..60464d37c4a6f0 100644
--- a/metadata-service/war/src/main/resources/boot/policies.json
+++ b/metadata-service/war/src/main/resources/boot/policies.json
@@ -232,6 +232,7 @@
"GET_TIMELINE_PRIVILEGE",
"PRODUCE_PLATFORM_EVENT_PRIVILEGE",
"MANAGE_DATA_PRODUCTS",
+ "CREATE_ER_MODEL_RELATIONSHIP_PRIVILEGE",
"DELETE_ENTITY",
"ES_EXPLAIN_QUERY_PRIVILEGE"
],
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 d6d2d24109874d..0f01f9dfcd5593 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
@@ -242,6 +242,12 @@ public class PoliciesConfig {
"Edit Embedded Content",
"The ability to edit the embedded content for an entity.");
+ public static final Privilege CREATE_ER_MODEL_RELATIONSHIP_PRIVILEGE =
+ Privilege.of(
+ "CREATE_ENTITY_ER_MODEL_RELATIONSHIP",
+ "Create erModelRelationship",
+ "The ability to add erModelRelationship on a dataset.");
+
public static final List COMMON_ENTITY_PRIVILEGES =
ImmutableList.of(
VIEW_ENTITY_PAGE_PRIVILEGE,
@@ -426,7 +432,8 @@ public class PoliciesConfig {
EDIT_ENTITY_ASSERTIONS_PRIVILEGE,
EDIT_LINEAGE_PRIVILEGE,
EDIT_ENTITY_EMBED_PRIVILEGE,
- EDIT_QUERIES_PRIVILEGE))
+ EDIT_QUERIES_PRIVILEGE,
+ CREATE_ER_MODEL_RELATIONSHIP_PRIVILEGE))
.flatMap(Collection::stream)
.collect(Collectors.toList()));
@@ -590,6 +597,13 @@ public class PoliciesConfig {
EDIT_USER_PROFILE_PRIVILEGE,
EDIT_ENTITY_PRIVILEGE));
+ // ERModelRelationship Privileges
+ public static final ResourcePrivileges ER_MODEL_RELATIONSHIP_PRIVILEGES =
+ ResourcePrivileges.of(
+ "erModelRelationship",
+ "ERModelRelationship",
+ "update privileges for ermodelrelations",
+ COMMON_ENTITY_PRIVILEGES);
public static final List ENTITY_RESOURCE_PRIVILEGES =
ImmutableList.of(
DATASET_PRIVILEGES,
@@ -605,7 +619,8 @@ public class PoliciesConfig {
CORP_GROUP_PRIVILEGES,
CORP_USER_PRIVILEGES,
NOTEBOOK_PRIVILEGES,
- DATA_PRODUCT_PRIVILEGES);
+ DATA_PRODUCT_PRIVILEGES,
+ ER_MODEL_RELATIONSHIP_PRIVILEGES);
// Merge all entity specific resource privileges to create a superset of all resource privileges
public static final ResourcePrivileges ALL_RESOURCE_PRIVILEGES =