diff --git a/.github/scripts/check_event_type.py b/.github/scripts/check_event_type.py index f575164a07fc1..c936497a2d307 100644 --- a/.github/scripts/check_event_type.py +++ b/.github/scripts/check_event_type.py @@ -1,7 +1,7 @@ import sys java_events = set() -with open("./metadata-io/src/main/java/com/linkedin/metadata/datahubusage/DataHubUsageEventType.java") as java_file: +with open("./metadata-service/services/src/main/java/com/linkedin/metadata/datahubusage/DataHubUsageEventType.java") as java_file: for line in java_file: if '''Event"''' not in line: continue diff --git a/build.gradle b/build.gradle index a13965e057a7c..605b4fcc050e7 100644 --- a/build.gradle +++ b/build.gradle @@ -129,6 +129,7 @@ project.ext.externalDependency = [ 'jsonSimple': 'com.googlecode.json-simple:json-simple:1.1.1', 'jsonSmart': 'net.minidev:json-smart:2.4.9', 'json': 'org.json:json:20230227', + 'junit': 'junit:junit:4.13.2', 'junitJupiterApi': "org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion", 'junitJupiterParams': "org.junit.jupiter:junit-jupiter-params:$junitJupiterVersion", 'junitJupiterEngine': "org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion", diff --git a/datahub-graphql-core/build.gradle b/datahub-graphql-core/build.gradle index 12ce7c090c869..8fd45033373dc 100644 --- a/datahub-graphql-core/build.gradle +++ b/datahub-graphql-core/build.gradle @@ -7,6 +7,8 @@ dependencies { compile project(':metadata-service:restli-client') compile project(':metadata-service:auth-impl') compile project(':metadata-service:auth-config') + compile project(':metadata-service:configuration') + compile project(':metadata-service:services') compile project(':metadata-io') compile project(':metadata-utils') 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 81279cac27ce0..f22568602d6b4 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 @@ -37,7 +37,6 @@ import com.linkedin.datahub.graphql.generated.CorpUser; import com.linkedin.datahub.graphql.generated.CorpUserInfo; import com.linkedin.datahub.graphql.generated.CorpUserViewsSettings; -import com.linkedin.datahub.graphql.generated.OwnershipTypeEntity; import com.linkedin.datahub.graphql.generated.Dashboard; import com.linkedin.datahub.graphql.generated.DashboardInfo; import com.linkedin.datahub.graphql.generated.DashboardStatsSummary; @@ -63,9 +62,9 @@ import com.linkedin.datahub.graphql.generated.InstitutionalMemoryMetadata; import com.linkedin.datahub.graphql.generated.LineageRelationship; import com.linkedin.datahub.graphql.generated.ListAccessTokenResult; -import com.linkedin.datahub.graphql.generated.ListOwnershipTypesResult; import com.linkedin.datahub.graphql.generated.ListDomainsResult; import com.linkedin.datahub.graphql.generated.ListGroupsResult; +import com.linkedin.datahub.graphql.generated.ListOwnershipTypesResult; import com.linkedin.datahub.graphql.generated.ListQueriesResult; import com.linkedin.datahub.graphql.generated.ListTestsResult; import com.linkedin.datahub.graphql.generated.ListViewsResult; @@ -80,6 +79,7 @@ import com.linkedin.datahub.graphql.generated.MLPrimaryKeyProperties; import com.linkedin.datahub.graphql.generated.Notebook; import com.linkedin.datahub.graphql.generated.Owner; +import com.linkedin.datahub.graphql.generated.OwnershipTypeEntity; import com.linkedin.datahub.graphql.generated.PolicyMatchCriterionValue; import com.linkedin.datahub.graphql.generated.QueryEntity; import com.linkedin.datahub.graphql.generated.QuerySubject; @@ -196,9 +196,9 @@ import com.linkedin.datahub.graphql.resolvers.mutate.UpdateParentNodeResolver; import com.linkedin.datahub.graphql.resolvers.mutate.UpdateUserSettingResolver; import com.linkedin.datahub.graphql.resolvers.operation.ReportOperationResolver; +import com.linkedin.datahub.graphql.resolvers.ownership.CreateOwnershipTypeResolver; import com.linkedin.datahub.graphql.resolvers.ownership.DeleteOwnershipTypeResolver; import com.linkedin.datahub.graphql.resolvers.ownership.ListOwnershipTypesResolver; -import com.linkedin.datahub.graphql.resolvers.ownership.CreateOwnershipTypeResolver; import com.linkedin.datahub.graphql.resolvers.ownership.UpdateOwnershipTypeResolver; import com.linkedin.datahub.graphql.resolvers.policy.DeletePolicyResolver; import com.linkedin.datahub.graphql.resolvers.policy.GetGrantedPrivilegesResolver; @@ -280,6 +280,7 @@ import com.linkedin.datahub.graphql.types.dataset.VersionedDatasetType; import com.linkedin.datahub.graphql.types.dataset.mappers.DatasetProfileMapper; import com.linkedin.datahub.graphql.types.domain.DomainType; +import com.linkedin.datahub.graphql.types.rolemetadata.RoleType; import com.linkedin.datahub.graphql.types.glossary.GlossaryNodeType; import com.linkedin.datahub.graphql.types.glossary.GlossaryTermType; import com.linkedin.datahub.graphql.types.mlmodel.MLFeatureTableType; @@ -302,6 +303,7 @@ import com.linkedin.metadata.config.TestsConfiguration; import com.linkedin.metadata.config.ViewsConfiguration; import com.linkedin.metadata.config.VisualConfiguration; +import com.linkedin.metadata.config.telemetry.TelemetryConfiguration; import com.linkedin.metadata.entity.EntityService; import com.linkedin.metadata.graph.GraphClient; import com.linkedin.metadata.graph.SiblingGraphService; @@ -311,12 +313,11 @@ import com.linkedin.metadata.recommendation.RecommendationsService; import com.linkedin.metadata.secret.SecretService; import com.linkedin.metadata.service.DataProductService; +import com.linkedin.metadata.service.LineageService; import com.linkedin.metadata.service.OwnershipTypeService; import com.linkedin.metadata.service.QueryService; import com.linkedin.metadata.service.SettingsService; import com.linkedin.metadata.service.ViewService; -import com.linkedin.metadata.service.LineageService; -import com.linkedin.metadata.telemetry.TelemetryConfiguration; import com.linkedin.metadata.timeline.TimelineService; import com.linkedin.metadata.timeseries.TimeseriesAspectService; import com.linkedin.metadata.version.GitVersion; @@ -330,6 +331,7 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -338,6 +340,7 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.dataloader.BatchLoaderContextProvider; @@ -353,6 +356,7 @@ * A {@link GraphQLEngine} configured to provide access to the entities and aspects on the the GMS graph. */ @Slf4j +@Getter public class GmsGraphQLEngine { private final EntityClient entityClient; @@ -394,6 +398,9 @@ public class GmsGraphQLEngine { private final ViewsConfiguration viewsConfiguration; private final DatasetType datasetType; + + private final RoleType roleType; + private final CorpUserType corpUserType; private final CorpGroupType corpGroupType; private final ChartType chartType; @@ -426,6 +433,11 @@ public class GmsGraphQLEngine { private final DataProductType dataProductType; private final OwnershipType ownershipType; + /** + * A list of GraphQL Plugins that extend the core engine + */ + private final List graphQLPlugins; + /** * Configures the graph objects that can be fetched primary key. */ @@ -453,6 +465,12 @@ public class GmsGraphQLEngine { public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) { + this.graphQLPlugins = List.of( + // Add new plugins here + ); + + this.graphQLPlugins.forEach(plugin -> plugin.init(args)); + this.entityClient = args.entityClient; this.graphClient = args.graphClient; this.usageClient = args.usageClient; @@ -491,6 +509,7 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) { this.featureFlags = args.featureFlags; this.datasetType = new DatasetType(entityClient); + this.roleType = new RoleType(entityClient); this.corpUserType = new CorpUserType(entityClient, featureFlags); this.corpGroupType = new CorpGroupType(entityClient); this.chartType = new ChartType(entityClient); @@ -526,6 +545,7 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) { // Init Lists this.entityTypes = ImmutableList.of( datasetType, + roleType, corpUserType, corpGroupType, dataPlatformType, @@ -558,6 +578,14 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) { ownershipType ); this.loadableTypes = new ArrayList<>(entityTypes); + // Extend loadable types with types from the plugins + // This allows us to offer search and browse capabilities out of the box for those types + for (GmsGraphQLPlugin plugin: this.graphQLPlugins) { + Collection> pluginLoadableTypes = plugin.getLoadableTypes(); + if (pluginLoadableTypes != null) { + this.loadableTypes.addAll(pluginLoadableTypes); + } + } this.ownerTypes = ImmutableList.of(corpUserType, corpGroupType); this.searchableTypes = loadableTypes.stream() .filter(type -> (type instanceof SearchableEntityType)) @@ -573,7 +601,7 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) { * Returns a {@link Supplier} responsible for creating a new {@link DataLoader} from * a {@link LoadableType}. */ - public Map>> loaderSuppliers(final List> loadableTypes) { + public Map>> loaderSuppliers(final Collection> loadableTypes) { return loadableTypes .stream() .collect(Collectors.toMap( @@ -582,6 +610,15 @@ public GmsGraphQLEngine(final GmsGraphQLEngineArgs args) { )); } + /** + * Final call to wire up any extra resolvers the plugin might want to add on + * @param builder + */ + private void configurePluginResolvers(final RuntimeWiring.Builder builder) { + this.graphQLPlugins.forEach(plugin -> plugin.configureExtraResolvers(builder, this)); + } + + public void configureRuntimeWiring(final RuntimeWiring.Builder builder) { configureQueryResolvers(builder); configureMutationResolvers(builder); @@ -605,6 +642,7 @@ public void configureRuntimeWiring(final RuntimeWiring.Builder builder) { configureContainerResolvers(builder); configureDataPlatformInstanceResolvers(builder); configureGlossaryTermResolvers(builder); + configureOrganisationRoleResolvers(builder); configureGlossaryNodeResolvers(builder); configureDomainResolvers(builder); configureAssertionResolvers(builder); @@ -619,10 +657,30 @@ public void configureRuntimeWiring(final RuntimeWiring.Builder builder) { configureViewResolvers(builder); configureQueryEntityResolvers(builder); configureOwnershipTypeResolver(builder); + configurePluginResolvers(builder); + } + + private void configureOrganisationRoleResolvers(RuntimeWiring.Builder builder) { + builder.type("Role", typeWiring -> typeWiring + .dataFetcher("relationships", new EntityRelationshipsResultResolver(graphClient)) + ); + builder.type("RoleAssociation", typeWiring -> typeWiring + .dataFetcher("role", + new LoadableTypeResolver<>(roleType, + (env) -> ((com.linkedin.datahub.graphql.generated.RoleAssociation) + env.getSource()).getRole().getUrn())) + ); + builder.type("RoleUser", typeWiring -> typeWiring + .dataFetcher("user", + new LoadableTypeResolver<>(corpUserType, + (env) -> ((com.linkedin.datahub.graphql.generated.RoleUser) + env.getSource()).getUser().getUrn())) + ); } public GraphQLEngine.Builder builder() { - return GraphQLEngine.builder() + final GraphQLEngine.Builder builder = GraphQLEngine.builder(); + builder .addSchema(fileBasedSchema(GMS_SCHEMA_FILE)) .addSchema(fileBasedSchema(SEARCH_SCHEMA_FILE)) .addSchema(fileBasedSchema(APP_SCHEMA_FILE)) @@ -633,10 +691,23 @@ public GraphQLEngine.Builder builder() { .addSchema(fileBasedSchema(TIMELINE_SCHEMA_FILE)) .addSchema(fileBasedSchema(TESTS_SCHEMA_FILE)) .addSchema(fileBasedSchema(STEPS_SCHEMA_FILE)) - .addSchema(fileBasedSchema(LINEAGE_SCHEMA_FILE)) + .addSchema(fileBasedSchema(LINEAGE_SCHEMA_FILE)); + + for (GmsGraphQLPlugin plugin: this.graphQLPlugins) { + List pluginSchemaFiles = plugin.getSchemaFiles(); + if (pluginSchemaFiles != null) { + pluginSchemaFiles.forEach(schema -> builder.addSchema(fileBasedSchema(schema))); + } + Collection> pluginLoadableTypes = plugin.getLoadableTypes(); + if (pluginLoadableTypes != null) { + pluginLoadableTypes.forEach(loadableType -> builder.addDataLoaders(loaderSuppliers(pluginLoadableTypes))); + } + } + builder .addDataLoaders(loaderSuppliers(loadableTypes)) .addDataLoader("Aspect", context -> createDataLoader(aspectType, context)) .configureRuntimeWiring(this::configureRuntimeWiring); + return builder; } public static String fileBasedSchema(String fileName) { @@ -726,6 +797,7 @@ private void configureQueryResolvers(final RuntimeWiring.Builder builder) { .dataFetcher("browse", new BrowseResolver(browsableTypes)) .dataFetcher("browsePaths", new BrowsePathsResolver(browsableTypes)) .dataFetcher("dataset", getResolver(datasetType)) + .dataFetcher("role", getResolver(roleType)) .dataFetcher("versionedDataset", getResolver(versionedDatasetType, (env) -> new VersionedUrn().setUrn(UrnUtils.getUrn(env.getArgument(URN_FIELD_NAME))) .setVersionStamp(env.getArgument(VERSION_STAMP_FIELD_NAME)))) @@ -1634,6 +1706,9 @@ private void configurePolicyResolvers(final RuntimeWiring.Builder builder) { })).dataFetcher("resolvedRoles", new LoadableTypeBatchResolver<>(dataHubRoleType, (env) -> { final ActorFilter filter = env.getSource(); return filter.getRoles(); + })).dataFetcher("resolvedOwnershipTypes", new LoadableTypeBatchResolver<>(ownershipType, (env) -> { + final ActorFilter filter = env.getSource(); + return filter.getResourceOwnersTypes(); }))); } 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 1c4cd09b329d6..cbcf42c4f93d9 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 @@ -16,6 +16,7 @@ import com.linkedin.metadata.config.TestsConfiguration; import com.linkedin.metadata.config.ViewsConfiguration; import com.linkedin.metadata.config.VisualConfiguration; +import com.linkedin.metadata.config.telemetry.TelemetryConfiguration; import com.linkedin.metadata.entity.EntityService; import com.linkedin.metadata.graph.GraphClient; import com.linkedin.metadata.graph.SiblingGraphService; @@ -28,7 +29,6 @@ import com.linkedin.metadata.service.QueryService; import com.linkedin.metadata.service.SettingsService; import com.linkedin.metadata.service.ViewService; -import com.linkedin.metadata.telemetry.TelemetryConfiguration; import com.linkedin.metadata.timeline.TimelineService; import com.linkedin.metadata.timeseries.TimeseriesAspectService; import com.linkedin.metadata.version.GitVersion; diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLPlugin.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLPlugin.java new file mode 100644 index 0000000000000..e7ef0c402a1de --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLPlugin.java @@ -0,0 +1,45 @@ +package com.linkedin.datahub.graphql; + +import com.linkedin.datahub.graphql.types.LoadableType; +import graphql.schema.idl.RuntimeWiring; +import java.util.Collection; +import java.util.List; + + +/** + * An interface that allows the Core GMS GraphQL Engine to be extended without requiring + * code changes in the GmsGraphQLEngine class if new entities, relationships or resolvers + * need to be introduced. This is useful if you are maintaining a fork of DataHub and + * don't want to deal with merge conflicts. + */ +public interface GmsGraphQLPlugin { + + /** + * Initialization method that allows the plugin to instantiate + * @param args + */ + void init(GmsGraphQLEngineArgs args); + + /** + * Return a list of schema files that contain graphql definitions + * that are served by this plugin + * @return + */ + List getSchemaFiles(); + + /** + * Return a list of LoadableTypes that this plugin serves + * @return + */ + Collection> getLoadableTypes(); + + /** + * Optional callback that a plugin can implement to configure any Query, Mutation or Type specific resolvers. + * @param wiringBuilder : the builder being used to configure the runtime wiring + * @param baseEngine : a reference to the core engine and its graphql types + */ + default void configureExtraResolvers(final RuntimeWiring.Builder wiringBuilder, final GmsGraphQLEngine baseEngine) { + + } + +} 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 49af7265ec9d7..f813562945378 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 @@ -14,4 +14,5 @@ public class FeatureFlags { private boolean showSearchFiltersV2 = false; private boolean showBrowseV2 = false; private PreProcessHooks preProcessHooks; + private boolean showAcrylInfo = false; } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/EntityTypeMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/EntityTypeMapper.java index 9a72d7dc2c77d..3682b2282544e 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/EntityTypeMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/EntityTypeMapper.java @@ -16,6 +16,7 @@ public class EntityTypeMapper { static final Map ENTITY_TYPE_TO_NAME = ImmutableMap.builder() .put(EntityType.DATASET, "dataset") + .put(EntityType.ROLE, "role") .put(EntityType.CORP_USER, "corpuser") .put(EntityType.CORP_GROUP, "corpGroup") .put(EntityType.DATA_PLATFORM, "dataPlatform") diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/chart/BrowseV2Resolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/chart/BrowseV2Resolver.java index 41a1d22485ea4..76abddc9a99a9 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/chart/BrowseV2Resolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/chart/BrowseV2Resolver.java @@ -49,6 +49,8 @@ public CompletableFuture get(DataFetchingEnvironment environmen final int start = input.getStart() != null ? input.getStart() : DEFAULT_START; final int count = input.getCount() != null ? input.getCount() : DEFAULT_COUNT; final String query = input.getQuery() != null ? input.getQuery() : "*"; + // escape forward slash since it is a reserved character in Elasticsearch + final String sanitizedQuery = ResolverUtils.escapeForwardSlash(query); return CompletableFuture.supplyAsync(() -> { try { @@ -64,7 +66,7 @@ public CompletableFuture get(DataFetchingEnvironment environmen maybeResolvedView != null ? SearchUtils.combineFilters(filter, maybeResolvedView.getDefinition().getFilter()) : filter, - query, + sanitizedQuery, start, count, context.getAuthentication() 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 48f0617c9975d..2c55bc79fe501 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 @@ -7,6 +7,8 @@ import com.linkedin.datahub.graphql.generated.AnalyticsConfig; import com.linkedin.datahub.graphql.generated.AppConfig; import com.linkedin.datahub.graphql.generated.AuthConfig; +import com.linkedin.datahub.graphql.generated.EntityProfileConfig; +import com.linkedin.datahub.graphql.generated.EntityProfilesConfig; import com.linkedin.datahub.graphql.generated.EntityType; import com.linkedin.datahub.graphql.generated.FeatureFlagsConfig; import com.linkedin.datahub.graphql.generated.IdentityManagementConfig; @@ -24,8 +26,8 @@ import com.linkedin.metadata.config.IngestionConfiguration; import com.linkedin.metadata.config.TestsConfiguration; import com.linkedin.metadata.config.ViewsConfiguration; -import com.linkedin.metadata.telemetry.TelemetryConfiguration; import com.linkedin.metadata.config.VisualConfiguration; +import com.linkedin.metadata.config.telemetry.TelemetryConfiguration; import com.linkedin.metadata.version.GitVersion; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; @@ -133,6 +135,15 @@ public CompletableFuture get(final DataFetchingEnvironment environmen queriesTabConfig.setQueriesTabResultSize(_visualConfiguration.getQueriesTab().getQueriesTabResultSize()); visualConfig.setQueriesTab(queriesTabConfig); } + if (_visualConfiguration != null && _visualConfiguration.getEntityProfile() != null) { + EntityProfilesConfig entityProfilesConfig = new EntityProfilesConfig(); + if (_visualConfiguration.getEntityProfile().getDomainDefaultTab() != null) { + EntityProfileConfig profileConfig = new EntityProfileConfig(); + profileConfig.setDefaultTab(_visualConfiguration.getEntityProfile().getDomainDefaultTab()); + entityProfilesConfig.setDomain(profileConfig); + } + visualConfig.setEntityProfiles(entityProfilesConfig); + } appConfig.setVisualConfig(visualConfig); final TelemetryConfig telemetryConfig = new TelemetryConfig(); @@ -151,6 +162,7 @@ public CompletableFuture get(final DataFetchingEnvironment environmen .setShowSearchFiltersV2(_featureFlags.isShowSearchFiltersV2()) .setReadOnlyModeEnabled(_featureFlags.isReadOnlyModeEnabled()) .setShowBrowseV2(_featureFlags.isShowBrowseV2()) + .setShowAcrylInfo(_featureFlags.isShowAcrylInfo()) .build(); appConfig.setFeatureFlags(featureFlagsConfig); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/DeleteDomainResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/DeleteDomainResolver.java index ba712f8e6c749..60a03fcddcc4d 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/DeleteDomainResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/domain/DeleteDomainResolver.java @@ -5,7 +5,6 @@ import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; import com.linkedin.datahub.graphql.exception.AuthorizationException; import com.linkedin.entity.client.EntityClient; -import com.linkedin.r2.RemoteInvocationException; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import java.util.concurrent.CompletableFuture; @@ -40,7 +39,7 @@ public CompletableFuture get(final DataFetchingEnvironment environment) CompletableFuture.runAsync(() -> { try { _entityClient.deleteEntityReferences(urn, context.getAuthentication()); - } catch (RemoteInvocationException e) { + } catch (Exception e) { log.error(String.format("Caught exception while attempting to clear all entity references for Domain with urn %s", urn), e); } }); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/glossary/CreateGlossaryTermResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/glossary/CreateGlossaryTermResolver.java index e454fe3aa4f67..ad69e0c5876e2 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/glossary/CreateGlossaryTermResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/glossary/CreateGlossaryTermResolver.java @@ -3,6 +3,7 @@ import com.linkedin.common.urn.GlossaryNodeUrn; import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.UrnUtils; +import com.linkedin.data.DataMap; import com.linkedin.data.template.SetMode; import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.exception.AuthorizationException; @@ -11,10 +12,15 @@ import com.linkedin.datahub.graphql.generated.OwnershipType; import com.linkedin.datahub.graphql.resolvers.mutate.util.GlossaryUtils; import com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils; +import com.linkedin.entity.EntityResponse; import com.linkedin.entity.client.EntityClient; import com.linkedin.glossary.GlossaryTermInfo; import com.linkedin.metadata.entity.EntityService; import com.linkedin.metadata.key.GlossaryTermKey; +import com.linkedin.metadata.query.filter.Filter; +import com.linkedin.metadata.search.SearchEntity; +import com.linkedin.metadata.search.SearchResult; +import com.linkedin.metadata.search.utils.QueryUtils; import com.linkedin.metadata.utils.EntityKeyUtils; import com.linkedin.mxe.MetadataChangeProposal; import graphql.schema.DataFetcher; @@ -23,8 +29,14 @@ import lombok.extern.slf4j.Slf4j; import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; import static com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils.*; @@ -36,6 +48,8 @@ @RequiredArgsConstructor public class CreateGlossaryTermResolver implements DataFetcher> { + static final String PARENT_NODE_INDEX_FIELD_NAME = "parentNode.keyword"; + private final EntityClient _entityClient; private final EntityService _entityService; @@ -48,6 +62,8 @@ public CompletableFuture get(DataFetchingEnvironment environment) throws return CompletableFuture.supplyAsync(() -> { if (GlossaryUtils.canManageChildrenEntities(context, parentNode, _entityClient)) { + // Ensure there isn't another glossary term with the same name at this level of the glossary + validateGlossaryTermName(parentNode, context, input.getName()); try { final GlossaryTermKey key = new GlossaryTermKey(); @@ -95,4 +111,50 @@ private GlossaryTermInfo mapGlossaryTermInfo(final CreateGlossaryEntityInput inp } return result; } + + private Filter buildParentNodeFilter(final Urn parentNodeUrn) { + final Map criterionMap = new HashMap<>(); + criterionMap.put(PARENT_NODE_INDEX_FIELD_NAME, parentNodeUrn == null ? null : parentNodeUrn.toString()); + return QueryUtils.newFilter(criterionMap); + } + + private Map getTermsWithSameParent(Urn parentNode, QueryContext context) { + try { + final Filter filter = buildParentNodeFilter(parentNode); + final SearchResult searchResult = _entityClient.filter( + GLOSSARY_TERM_ENTITY_NAME, + filter, + null, + 0, + 1000, + context.getAuthentication()); + + final List termUrns = searchResult.getEntities() + .stream() + .map(SearchEntity::getEntity) + .collect(Collectors.toList()); + + return _entityClient.batchGetV2( + GLOSSARY_TERM_ENTITY_NAME, + new HashSet<>(termUrns), + Collections.singleton(GLOSSARY_TERM_INFO_ASPECT_NAME), + context.getAuthentication()); + } catch (Exception e) { + throw new RuntimeException("Failed fetching Glossary Terms with the same parent", e); + } + } + + private void validateGlossaryTermName(Urn parentNode, QueryContext context, String name) { + Map entities = getTermsWithSameParent(parentNode, context); + + entities.forEach((urn, entityResponse) -> { + if (entityResponse.getAspects().containsKey(GLOSSARY_TERM_INFO_ASPECT_NAME)) { + DataMap dataMap = entityResponse.getAspects().get(GLOSSARY_TERM_INFO_ASPECT_NAME).getValue().data(); + GlossaryTermInfo termInfo = new GlossaryTermInfo(dataMap); + if (termInfo.hasName() && termInfo.getName().equals(name)) { + throw new IllegalArgumentException("Glossary Term with this name already exists at this level of the Business Glossary"); + } + } + }); + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/glossary/DeleteGlossaryEntityResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/glossary/DeleteGlossaryEntityResolver.java index 4fd7a57a8a413..0929c7138528d 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/glossary/DeleteGlossaryEntityResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/glossary/DeleteGlossaryEntityResolver.java @@ -6,7 +6,6 @@ import com.linkedin.datahub.graphql.resolvers.mutate.util.GlossaryUtils; import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.entity.EntityService; -import com.linkedin.r2.RemoteInvocationException; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import java.util.concurrent.CompletableFuture; @@ -43,7 +42,7 @@ public CompletableFuture get(final DataFetchingEnvironment environment) CompletableFuture.runAsync(() -> { try { _entityClient.deleteEntityReferences(entityUrn, context.getAuthentication()); - } catch (RemoteInvocationException e) { + } catch (Exception e) { log.error(String.format("Caught exception while attempting to clear all entity references for glossary entity with urn %s", entityUrn), e); } }); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/group/RemoveGroupResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/group/RemoveGroupResolver.java index 99b75ea9d90cd..99481868e30ce 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/group/RemoveGroupResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/group/RemoveGroupResolver.java @@ -5,7 +5,6 @@ import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; import com.linkedin.datahub.graphql.exception.AuthorizationException; import com.linkedin.entity.client.EntityClient; -import com.linkedin.r2.RemoteInvocationException; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import java.util.concurrent.CompletableFuture; @@ -38,7 +37,7 @@ public CompletableFuture get(final DataFetchingEnvironment environment) CompletableFuture.runAsync(() -> { try { _entityClient.deleteEntityReferences(urn, context.getAuthentication()); - } catch (RemoteInvocationException e) { + } catch (Exception e) { log.error(String.format("Caught exception while attempting to clear all entity references for group with urn %s", urn), e); } }); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/ingest/execution/CreateTestConnectionRequestResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/ingest/execution/CreateTestConnectionRequestResolver.java index 8f4d538ca67ec..1886db62ae450 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/ingest/execution/CreateTestConnectionRequestResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/ingest/execution/CreateTestConnectionRequestResolver.java @@ -1,5 +1,6 @@ package com.linkedin.datahub.graphql.resolvers.ingest.execution; +import com.linkedin.metadata.config.IngestionConfiguration; import com.linkedin.common.urn.Urn; import com.linkedin.data.template.StringMap; import com.linkedin.datahub.graphql.QueryContext; @@ -9,7 +10,6 @@ import com.linkedin.entity.client.EntityClient; import com.linkedin.execution.ExecutionRequestInput; import com.linkedin.execution.ExecutionRequestSource; -import com.linkedin.metadata.config.IngestionConfiguration; import com.linkedin.metadata.key.ExecutionRequestKey; import com.linkedin.metadata.utils.EntityKeyUtils; import com.linkedin.metadata.utils.IngestionUtils; diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/LabelUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/LabelUtils.java index 1922a02fc1ca0..e2dbf1d3f9c99 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/LabelUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/LabelUtils.java @@ -135,7 +135,8 @@ public static void addTermsToResource( ) throws URISyntaxException { if (subResource == null || subResource.equals("")) { com.linkedin.common.GlossaryTerms terms = - (com.linkedin.common.GlossaryTerms) getAspectFromEntity(resourceUrn.toString(), GLOSSARY_TERM_ASPECT_NAME, entityService, new GlossaryTerms()); + (com.linkedin.common.GlossaryTerms) getAspectFromEntity(resourceUrn.toString(), GLOSSARY_TERM_ASPECT_NAME, + entityService, new GlossaryTerms()); terms.setAuditStamp(getAuditStamp(actor)); if (!terms.hasTerms()) { @@ -320,7 +321,8 @@ private static MetadataChangeProposal buildRemoveTagsToEntityProposal( EntityService entityService ) { com.linkedin.common.GlobalTags tags = - (com.linkedin.common.GlobalTags) getAspectFromEntity(resource.getResourceUrn(), TAGS_ASPECT_NAME, entityService, new GlobalTags()); + (com.linkedin.common.GlobalTags) getAspectFromEntity(resource.getResourceUrn(), TAGS_ASPECT_NAME, + entityService, new GlobalTags()); if (!tags.hasTags()) { tags.setTags(new TagAssociationArray()); @@ -357,7 +359,8 @@ private static MetadataChangeProposal buildAddTagsToEntityProposal( EntityService entityService ) throws URISyntaxException { com.linkedin.common.GlobalTags tags = - (com.linkedin.common.GlobalTags) getAspectFromEntity(resource.getResourceUrn(), TAGS_ASPECT_NAME, entityService, new GlobalTags()); + (com.linkedin.common.GlobalTags) getAspectFromEntity(resource.getResourceUrn(), TAGS_ASPECT_NAME, + entityService, new GlobalTags()); if (!tags.hasTags()) { tags.setTags(new TagAssociationArray()); @@ -449,7 +452,8 @@ private static MetadataChangeProposal buildAddTermsToEntityProposal( EntityService entityService ) throws URISyntaxException { com.linkedin.common.GlossaryTerms terms = - (com.linkedin.common.GlossaryTerms) getAspectFromEntity(resource.getResourceUrn(), GLOSSARY_TERM_ASPECT_NAME, entityService, new GlossaryTerms()); + (com.linkedin.common.GlossaryTerms) getAspectFromEntity(resource.getResourceUrn(), GLOSSARY_TERM_ASPECT_NAME, + entityService, new GlossaryTerms()); terms.setAuditStamp(getAuditStamp(actor)); if (!terms.hasTerms()) { diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/OwnerUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/OwnerUtils.java index a08419b5226b4..d8a92fb3f6607 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/OwnerUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/util/OwnerUtils.java @@ -72,8 +72,7 @@ public static void removeOwnersFromResources( private static MetadataChangeProposal buildAddOwnersProposal(List owners, Urn resourceUrn, Urn actor, EntityService entityService) { Ownership ownershipAspect = (Ownership) getAspectFromEntity( resourceUrn.toString(), - Constants.OWNERSHIP_ASPECT_NAME, - entityService, + Constants.OWNERSHIP_ASPECT_NAME, entityService, new Ownership()); for (OwnerInput input : owners) { addOwner(ownershipAspect, UrnUtils.getUrn(input.getOwnerUrn()), input.getType(), UrnUtils.getUrn(input.getOwnershipTypeUrn())); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyInfoPolicyMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyInfoPolicyMapper.java index fdcbb7eda8338..b9a6bf07be8c8 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyInfoPolicyMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyInfoPolicyMapper.java @@ -1,7 +1,7 @@ package com.linkedin.datahub.graphql.resolvers.policy.mappers; +import com.linkedin.common.UrnArray; import com.linkedin.common.urn.Urn; -import com.linkedin.datahub.graphql.generated.ActorFilter; import com.linkedin.datahub.graphql.generated.Policy; import com.linkedin.datahub.graphql.generated.PolicyMatchCondition; import com.linkedin.datahub.graphql.generated.PolicyMatchCriterion; @@ -9,6 +9,7 @@ import com.linkedin.datahub.graphql.generated.PolicyMatchFilter; import com.linkedin.datahub.graphql.generated.PolicyState; import com.linkedin.datahub.graphql.generated.PolicyType; +import com.linkedin.datahub.graphql.generated.ActorFilter; import com.linkedin.datahub.graphql.generated.ResourceFilter; import com.linkedin.datahub.graphql.types.common.mappers.UrnToEntityMapper; import com.linkedin.datahub.graphql.types.mappers.ModelMapper; @@ -53,6 +54,10 @@ private ActorFilter mapActors(final DataHubActorFilter actorFilter) { result.setAllGroups(actorFilter.isAllGroups()); result.setAllUsers(actorFilter.isAllUsers()); result.setResourceOwners(actorFilter.isResourceOwners()); + UrnArray resourceOwnersTypes = actorFilter.getResourceOwnersTypes(); + if (resourceOwnersTypes != null) { + result.setResourceOwnersTypes(resourceOwnersTypes.stream().map(Urn::toString).collect(Collectors.toList())); + } if (actorFilter.hasGroups()) { result.setGroups(actorFilter.getGroups().stream().map(Urn::toString).collect(Collectors.toList())); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyUpdateInputInfoMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyUpdateInputInfoMapper.java index 1e0f41c68b32e..cb323b60dd465 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyUpdateInputInfoMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyUpdateInputInfoMapper.java @@ -51,6 +51,9 @@ private DataHubActorFilter mapActors(final ActorFilterInput actorInput) { result.setAllGroups(actorInput.getAllGroups()); result.setAllUsers(actorInput.getAllUsers()); result.setResourceOwners(actorInput.getResourceOwners()); + if (actorInput.getResourceOwnersTypes() != null) { + result.setResourceOwnersTypes(new UrnArray(actorInput.getResourceOwnersTypes().stream().map(this::createUrn).collect(Collectors.toList()))); + } if (actorInput.getGroups() != null) { result.setGroups(new UrnArray(actorInput.getGroups().stream().map(this::createUrn).collect(Collectors.toList()))); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/GetQuickFiltersResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/GetQuickFiltersResolver.java index 782760aca744b..5f4f8dd974328 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/GetQuickFiltersResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/GetQuickFiltersResolver.java @@ -33,7 +33,6 @@ import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; import static com.linkedin.datahub.graphql.resolvers.search.SearchUtils.SEARCHABLE_ENTITY_TYPES; import static com.linkedin.datahub.graphql.resolvers.search.SearchUtils.resolveView; -import static com.linkedin.datahub.graphql.types.mappers.MapperUtils.*; @Slf4j diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java index 492380aa21482..3b516ce8ca8e6 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchUtils.java @@ -70,8 +70,10 @@ private SearchUtils() { EntityType.CORP_GROUP, EntityType.CONTAINER, EntityType.DOMAIN, - EntityType.NOTEBOOK, - EntityType.DATA_PRODUCT); + EntityType.DATA_PRODUCT, + EntityType.ROLE, + EntityType.NOTEBOOK); + /** * Entities that are part of autocomplete by default in Auto Complete Across Entities diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/tag/DeleteTagResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/tag/DeleteTagResolver.java index 72b95935838ef..e6c3cf49df8db 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/tag/DeleteTagResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/tag/DeleteTagResolver.java @@ -6,7 +6,6 @@ import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; import com.linkedin.datahub.graphql.exception.AuthorizationException; import com.linkedin.entity.client.EntityClient; -import com.linkedin.r2.RemoteInvocationException; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import java.util.concurrent.CompletableFuture; @@ -41,7 +40,7 @@ public CompletableFuture get(final DataFetchingEnvironment environment) CompletableFuture.runAsync(() -> { try { _entityClient.deleteEntityReferences(urn, context.getAuthentication()); - } catch (RemoteInvocationException e) { + } catch (Exception e) { log.error(String.format( "Caught exception while attempting to clear all entity references for Tag with urn %s", urn), e); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/user/RemoveUserResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/user/RemoveUserResolver.java index f77823d47ee9a..718810e4710e7 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/user/RemoveUserResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/user/RemoveUserResolver.java @@ -5,7 +5,6 @@ import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; import com.linkedin.datahub.graphql.exception.AuthorizationException; import com.linkedin.entity.client.EntityClient; -import com.linkedin.r2.RemoteInvocationException; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import java.util.concurrent.CompletableFuture; @@ -38,7 +37,7 @@ public CompletableFuture get(final DataFetchingEnvironment environment) CompletableFuture.runAsync(() -> { try { _entityClient.deleteEntityReferences(urn, context.getAuthentication()); - } catch (RemoteInvocationException e) { + } catch (Exception e) { log.error(String.format("Caught exception while attempting to clear all entity references for user with urn %s", urn), e); } }); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/chart/mappers/ChartMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/chart/mappers/ChartMapper.java index 8b525dd587753..9e03bf19889d1 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/chart/mappers/ChartMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/chart/mappers/ChartMapper.java @@ -82,7 +82,7 @@ public Chart apply(@Nonnull final EntityResponse entityResponse) { chart.setStatus(StatusMapper.map(new Status(dataMap)))); mappingHelper.mapToResult(GLOBAL_TAGS_ASPECT_NAME, (dataset, dataMap) -> this.mapGlobalTags(dataset, dataMap, entityUrn)); mappingHelper.mapToResult(INSTITUTIONAL_MEMORY_ASPECT_NAME, (chart, dataMap) -> - chart.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap)))); + chart.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap), entityUrn))); mappingHelper.mapToResult(GLOSSARY_TERMS_ASPECT_NAME, (chart, dataMap) -> chart.setGlossaryTerms(GlossaryTermsMapper.map(new GlossaryTerms(dataMap), entityUrn))); mappingHelper.mapToResult(CONTAINER_ASPECT_NAME, this::mapContainers); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/FineGrainedLineagesMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/FineGrainedLineagesMapper.java new file mode 100644 index 0000000000000..9f4517c89a6dc --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/FineGrainedLineagesMapper.java @@ -0,0 +1,53 @@ +package com.linkedin.datahub.graphql.types.common.mappers; + +import com.linkedin.common.urn.Urn; +import com.linkedin.datahub.graphql.generated.FineGrainedLineage; +import com.linkedin.datahub.graphql.generated.SchemaFieldRef; +import com.linkedin.dataset.FineGrainedLineageArray; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static com.linkedin.metadata.Constants.SCHEMA_FIELD_ENTITY_NAME; + +public class FineGrainedLineagesMapper { + + public static final FineGrainedLineagesMapper INSTANCE = new FineGrainedLineagesMapper(); + + public static List map(@Nonnull final FineGrainedLineageArray fineGrainedLineages) { + return INSTANCE.apply(fineGrainedLineages); + } + + public List apply(@Nonnull final FineGrainedLineageArray fineGrainedLineages) { + final List result = new ArrayList<>(); + if (fineGrainedLineages.size() == 0) { + return result; + } + + for (com.linkedin.dataset.FineGrainedLineage fineGrainedLineage : fineGrainedLineages) { + com.linkedin.datahub.graphql.generated.FineGrainedLineage resultEntry = new com.linkedin.datahub.graphql.generated.FineGrainedLineage(); + if (fineGrainedLineage.hasUpstreams()) { + resultEntry.setUpstreams(fineGrainedLineage.getUpstreams().stream() + .filter(entry -> entry.getEntityType().equals(SCHEMA_FIELD_ENTITY_NAME)) + .map(FineGrainedLineagesMapper::mapDatasetSchemaField).collect( + Collectors.toList())); + } + if (fineGrainedLineage.hasDownstreams()) { + resultEntry.setDownstreams(fineGrainedLineage.getDownstreams().stream() + .filter(entry -> entry.getEntityType().equals(SCHEMA_FIELD_ENTITY_NAME)) + .map(FineGrainedLineagesMapper::mapDatasetSchemaField).collect( + Collectors.toList())); + } + result.add(resultEntry); + } + return result; + } + + private static SchemaFieldRef mapDatasetSchemaField(final Urn schemaFieldUrn) { + return new SchemaFieldRef(schemaFieldUrn.getEntityKey().get(0), schemaFieldUrn.getEntityKey().get(1)); + } +} + + diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/InstitutionalMemoryMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/InstitutionalMemoryMapper.java index f4b7bfc4cd2bc..8bcfe7eb3b6d0 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/InstitutionalMemoryMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/InstitutionalMemoryMapper.java @@ -1,23 +1,23 @@ package com.linkedin.datahub.graphql.types.common.mappers; +import com.linkedin.common.urn.Urn; import com.linkedin.datahub.graphql.generated.InstitutionalMemory; -import com.linkedin.datahub.graphql.types.mappers.ModelMapper; import javax.annotation.Nonnull; import java.util.stream.Collectors; -public class InstitutionalMemoryMapper implements ModelMapper { +public class InstitutionalMemoryMapper { public static final InstitutionalMemoryMapper INSTANCE = new InstitutionalMemoryMapper(); - public static InstitutionalMemory map(@Nonnull final com.linkedin.common.InstitutionalMemory memory) { - return INSTANCE.apply(memory); + public static InstitutionalMemory map(@Nonnull final com.linkedin.common.InstitutionalMemory memory, @Nonnull final Urn entityUrn) { + return INSTANCE.apply(memory, entityUrn); } - @Override - public InstitutionalMemory apply(@Nonnull final com.linkedin.common.InstitutionalMemory input) { + public InstitutionalMemory apply(@Nonnull final com.linkedin.common.InstitutionalMemory input, @Nonnull final Urn entityUrn) { final InstitutionalMemory result = new InstitutionalMemory(); - result.setElements(input.getElements().stream().map(InstitutionalMemoryMetadataMapper::map).collect(Collectors.toList())); + result.setElements(input.getElements().stream().map(metadata -> + InstitutionalMemoryMetadataMapper.map(metadata, entityUrn)).collect(Collectors.toList())); return result; } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/InstitutionalMemoryMetadataMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/InstitutionalMemoryMetadataMapper.java index 89afcee0d788c..ba4d37173abb8 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/InstitutionalMemoryMetadataMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/InstitutionalMemoryMetadataMapper.java @@ -1,27 +1,27 @@ package com.linkedin.datahub.graphql.types.common.mappers; +import com.linkedin.common.urn.Urn; import com.linkedin.datahub.graphql.generated.InstitutionalMemoryMetadata; import com.linkedin.datahub.graphql.generated.CorpUser; -import com.linkedin.datahub.graphql.types.mappers.ModelMapper; import javax.annotation.Nonnull; -public class InstitutionalMemoryMetadataMapper implements ModelMapper { +public class InstitutionalMemoryMetadataMapper { public static final InstitutionalMemoryMetadataMapper INSTANCE = new InstitutionalMemoryMetadataMapper(); - public static InstitutionalMemoryMetadata map(@Nonnull final com.linkedin.common.InstitutionalMemoryMetadata metadata) { - return INSTANCE.apply(metadata); + public static InstitutionalMemoryMetadata map(@Nonnull final com.linkedin.common.InstitutionalMemoryMetadata metadata, @Nonnull final Urn entityUrn) { + return INSTANCE.apply(metadata, entityUrn); } - @Override - public InstitutionalMemoryMetadata apply(@Nonnull final com.linkedin.common.InstitutionalMemoryMetadata input) { + public InstitutionalMemoryMetadata apply(@Nonnull final com.linkedin.common.InstitutionalMemoryMetadata input, @Nonnull final Urn entityUrn) { final InstitutionalMemoryMetadata result = new InstitutionalMemoryMetadata(); result.setUrl(input.getUrl().toString()); result.setDescription(input.getDescription()); // deprecated field result.setLabel(input.getDescription()); result.setAuthor(getAuthor(input.getCreateStamp().getActor().toString())); result.setCreated(AuditStampMapper.map(input.getCreateStamp())); + result.setAssociatedUrn(entityUrn.toString()); return result; } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/OwnerUpdateMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/OwnerUpdateMapper.java index aac0067974a69..d978abee5bdfc 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/OwnerUpdateMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/OwnerUpdateMapper.java @@ -37,10 +37,17 @@ public Owner apply(@Nonnull final OwnerUpdate input) { } if (input.getOwnershipTypeUrn() != null) { owner.setTypeUrn(UrnUtils.getUrn(input.getOwnershipTypeUrn())); - } else if (input.getType() != null) { - owner.setType(OwnershipType.valueOf(input.getType().toString())); - } else { - throw new RuntimeException("Ownership type not specified. Please define the ownership type urn."); + } + // For backwards compatibility we have to always set the deprecated type. + // If the type exists we assume it's an old ownership type that we can map to. + // Else if it's a net new custom ownership type set old type to CUSTOM. + OwnershipType type = input.getType() != null ? OwnershipType.valueOf(input.getType().toString()) + : OwnershipType.CUSTOM; + owner.setType(type); + + if (input.getOwnershipTypeUrn() != null) { + owner.setTypeUrn(UrnUtils.getUrn(input.getOwnershipTypeUrn())); + owner.setType(OwnershipType.CUSTOM); } owner.setSource(new OwnershipSource().setType(OwnershipSourceType.SERVICE)); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/UpstreamLineagesMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/UpstreamLineagesMapper.java index 40f0ca90b0d9f..8359f1ec86f34 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/UpstreamLineagesMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/common/mappers/UpstreamLineagesMapper.java @@ -1,11 +1,7 @@ package com.linkedin.datahub.graphql.types.common.mappers; -import com.linkedin.common.urn.Urn; -import com.linkedin.datahub.graphql.generated.SchemaFieldRef; -import com.linkedin.dataset.FineGrainedLineage; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -23,31 +19,10 @@ public static List ma } public List apply(@Nonnull final com.linkedin.dataset.UpstreamLineage upstreamLineage) { - final List result = new ArrayList<>(); - if (!upstreamLineage.hasFineGrainedLineages()) { - return result; + if (!upstreamLineage.hasFineGrainedLineages() || upstreamLineage.getFineGrainedLineages() == null) { + return new ArrayList<>(); } - for (FineGrainedLineage fineGrainedLineage : upstreamLineage.getFineGrainedLineages()) { - com.linkedin.datahub.graphql.generated.FineGrainedLineage resultEntry = new com.linkedin.datahub.graphql.generated.FineGrainedLineage(); - if (fineGrainedLineage.hasUpstreams()) { - resultEntry.setUpstreams(fineGrainedLineage.getUpstreams().stream() - .filter(entry -> entry.getEntityType().equals("schemaField")) - .map(entry -> mapDatasetSchemaField(entry)).collect( - Collectors.toList())); - } - if (fineGrainedLineage.hasDownstreams()) { - resultEntry.setDownstreams(fineGrainedLineage.getDownstreams().stream() - .filter(entry -> entry.getEntityType().equals("schemaField")) - .map(entry -> mapDatasetSchemaField(entry)).collect( - Collectors.toList())); - } - result.add(resultEntry); - } - return result; - } - - private static SchemaFieldRef mapDatasetSchemaField(final Urn schemaFieldUrn) { - return new SchemaFieldRef(schemaFieldUrn.getEntityKey().get(0), schemaFieldUrn.getEntityKey().get(1)); + return FineGrainedLineagesMapper.map(upstreamLineage.getFineGrainedLineages()); } } 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 2f3204aae0afe..34bf56a396b62 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 @@ -28,6 +28,7 @@ import com.linkedin.datahub.graphql.generated.MLPrimaryKey; import com.linkedin.datahub.graphql.generated.Notebook; import com.linkedin.datahub.graphql.generated.OwnershipTypeEntity; +import com.linkedin.datahub.graphql.generated.Role; import com.linkedin.datahub.graphql.generated.SchemaFieldEntity; import com.linkedin.datahub.graphql.generated.Tag; import com.linkedin.datahub.graphql.generated.Test; @@ -52,6 +53,11 @@ public Entity apply(Urn input) { ((Dataset) partialEntity).setUrn(input.toString()); ((Dataset) partialEntity).setType(EntityType.DATASET); } + if (input.getEntityType().equals("role")) { + partialEntity = new Role(); + ((Role) partialEntity).setUrn(input.toString()); + ((Role) partialEntity).setType(EntityType.ROLE); + } if (input.getEntityType().equals("glossaryTerm")) { partialEntity = new GlossaryTerm(); ((GlossaryTerm) partialEntity).setUrn(input.toString()); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/container/mappers/ContainerMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/container/mappers/ContainerMapper.java index a61369e79224d..ec559a569920d 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/container/mappers/ContainerMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/container/mappers/ContainerMapper.java @@ -87,7 +87,7 @@ public static Container map(final EntityResponse entityResponse) { final EnvelopedAspect envelopedInstitutionalMemory = aspects.get(Constants.INSTITUTIONAL_MEMORY_ASPECT_NAME); if (envelopedInstitutionalMemory != null) { - result.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(envelopedInstitutionalMemory.getValue().data()))); + result.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(envelopedInstitutionalMemory.getValue().data()), entityUrn)); } final EnvelopedAspect statusAspect = aspects.get(Constants.STATUS_ASPECT_NAME); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dashboard/mappers/DashboardMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dashboard/mappers/DashboardMapper.java index d11767c7bf41d..38e2cacbde668 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dashboard/mappers/DashboardMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dashboard/mappers/DashboardMapper.java @@ -79,7 +79,7 @@ public Dashboard apply(@Nonnull final EntityResponse entityResponse) { mappingHelper.mapToResult(STATUS_ASPECT_NAME, (dashboard, dataMap) -> dashboard.setStatus(StatusMapper.map(new Status(dataMap)))); mappingHelper.mapToResult(INSTITUTIONAL_MEMORY_ASPECT_NAME, (dashboard, dataMap) -> - dashboard.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap)))); + dashboard.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap), entityUrn))); mappingHelper.mapToResult(GLOSSARY_TERMS_ASPECT_NAME, (dashboard, dataMap) -> dashboard.setGlossaryTerms(GlossaryTermsMapper.map(new GlossaryTerms(dataMap), entityUrn))); mappingHelper.mapToResult(CONTAINER_ASPECT_NAME, this::mapContainers); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataflow/mappers/DataFlowMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataflow/mappers/DataFlowMapper.java index eec52facf2a63..98debe08cf36b 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataflow/mappers/DataFlowMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataflow/mappers/DataFlowMapper.java @@ -70,7 +70,7 @@ public DataFlow apply(@Nonnull final EntityResponse entityResponse) { dataFlow.setStatus(StatusMapper.map(new Status(dataMap)))); mappingHelper.mapToResult(GLOBAL_TAGS_ASPECT_NAME, (dataFlow, dataMap) -> this.mapGlobalTags(dataFlow, dataMap, entityUrn)); mappingHelper.mapToResult(INSTITUTIONAL_MEMORY_ASPECT_NAME, (dataFlow, dataMap) -> - dataFlow.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap)))); + dataFlow.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap), entityUrn))); mappingHelper.mapToResult(GLOSSARY_TERMS_ASPECT_NAME, (dataFlow, dataMap) -> dataFlow.setGlossaryTerms(GlossaryTermsMapper.map(new GlossaryTerms(dataMap), entityUrn))); mappingHelper.mapToResult(DOMAINS_ASPECT_NAME, this::mapDomains); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datajob/mappers/DataJobMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datajob/mappers/DataJobMapper.java index e592ce2cdc2b5..208a85acfe42e 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datajob/mappers/DataJobMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datajob/mappers/DataJobMapper.java @@ -22,6 +22,7 @@ import com.linkedin.datahub.graphql.types.common.mappers.BrowsePathsV2Mapper; import com.linkedin.datahub.graphql.types.common.mappers.DataPlatformInstanceAspectMapper; import com.linkedin.datahub.graphql.types.common.mappers.DeprecationMapper; +import com.linkedin.datahub.graphql.types.common.mappers.FineGrainedLineagesMapper; 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; @@ -89,7 +90,7 @@ public DataJob apply(@Nonnull final EntityResponse entityResponse) { result.setGlobalTags(globalTags); result.setTags(globalTags); } else if (INSTITUTIONAL_MEMORY_ASPECT_NAME.equals(name)) { - result.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(data))); + result.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(data), entityUrn)); } else if (GLOSSARY_TERMS_ASPECT_NAME.equals(name)) { result.setGlossaryTerms(GlossaryTermsMapper.map(new GlossaryTerms(data), entityUrn)); } else if (DOMAINS_ASPECT_NAME.equals(name)) { @@ -170,6 +171,10 @@ private DataJobInputOutput mapDataJobInputOutput(final com.linkedin.datajob.Data result.setInputDatajobs(ImmutableList.of()); } + if (inputOutput.hasFineGrainedLineages() && inputOutput.getFineGrainedLineages() != null) { + result.setFineGrainedLineages(FineGrainedLineagesMapper.map(inputOutput.getFineGrainedLineages())); + } + return result; } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataplatforminstance/mappers/DataPlatformInstanceMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataplatforminstance/mappers/DataPlatformInstanceMapper.java index 6baa226abec1d..ba49f23133f9e 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataplatforminstance/mappers/DataPlatformInstanceMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataplatforminstance/mappers/DataPlatformInstanceMapper.java @@ -57,7 +57,7 @@ public DataPlatformInstance apply(@Nonnull final EntityResponse entityResponse) ); mappingHelper.mapToResult(Constants.INSTITUTIONAL_MEMORY_ASPECT_NAME, (dataPlatformInstance, dataMap) -> - dataPlatformInstance.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap))) + dataPlatformInstance.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap), entityUrn)) ); mappingHelper.mapToResult(Constants.STATUS_ASPECT_NAME, (dataPlatformInstance, dataMap) -> diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataproduct/mappers/DataProductMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataproduct/mappers/DataProductMapper.java index ff8f7dedaa0d3..9cb6840067e7b 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataproduct/mappers/DataProductMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataproduct/mappers/DataProductMapper.java @@ -60,7 +60,7 @@ public DataProduct apply(@Nonnull final EntityResponse entityResponse) { mappingHelper.mapToResult(OWNERSHIP_ASPECT_NAME, (dataProduct, dataMap) -> dataProduct.setOwnership(OwnershipMapper.map(new Ownership(dataMap), entityUrn))); mappingHelper.mapToResult(INSTITUTIONAL_MEMORY_ASPECT_NAME, (dataProduct, dataMap) -> - dataProduct.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap)))); + dataProduct.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap), entityUrn))); return result; } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java index 510971c8db11c..0fc4399ac902d 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/DatasetType.java @@ -85,7 +85,8 @@ public class DatasetType implements SearchableEntityType, Brows SIBLINGS_ASPECT_NAME, EMBED_ASPECT_NAME, DATA_PRODUCTS_ASPECT_NAME, - BROWSE_PATHS_V2_ASPECT_NAME + BROWSE_PATHS_V2_ASPECT_NAME, + ACCESS_DATASET_ASPECT_NAME ); private static final Set FACET_FIELDS = ImmutableSet.of("origin", "platform"); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/DatasetMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/DatasetMapper.java index c5b810951d491..f0899f8fbc0cb 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/DatasetMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/DatasetMapper.java @@ -1,5 +1,6 @@ package com.linkedin.datahub.graphql.types.dataset.mappers; +import com.linkedin.common.Access; import com.linkedin.common.BrowsePathsV2; import com.linkedin.common.DataPlatformInstance; import com.linkedin.common.Deprecation; @@ -32,6 +33,7 @@ import com.linkedin.datahub.graphql.types.common.mappers.util.MappingHelper; import com.linkedin.datahub.graphql.types.common.mappers.util.SystemMetadataUtils; import com.linkedin.datahub.graphql.types.domain.DomainAssociationMapper; +import com.linkedin.datahub.graphql.types.rolemetadata.mappers.AccessMapper; 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; @@ -86,7 +88,7 @@ public Dataset apply(@Nonnull final EntityResponse entityResponse) { mappingHelper.mapToResult(EDITABLE_DATASET_PROPERTIES_ASPECT_NAME, this::mapEditableDatasetProperties); mappingHelper.mapToResult(VIEW_PROPERTIES_ASPECT_NAME, this::mapViewProperties); mappingHelper.mapToResult(INSTITUTIONAL_MEMORY_ASPECT_NAME, (dataset, dataMap) -> - dataset.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap)))); + dataset.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap), entityUrn))); mappingHelper.mapToResult(OWNERSHIP_ASPECT_NAME, (dataset, dataMap) -> dataset.setOwnership(OwnershipMapper.map(new Ownership(dataMap), entityUrn))); mappingHelper.mapToResult(STATUS_ASPECT_NAME, (dataset, dataMap) -> @@ -110,6 +112,8 @@ public Dataset apply(@Nonnull final EntityResponse entityResponse) { dataset.setEmbed(EmbedMapper.map(new Embed(dataMap)))); mappingHelper.mapToResult(BROWSE_PATHS_V2_ASPECT_NAME, (dataset, dataMap) -> dataset.setBrowsePathV2(BrowsePathsV2Mapper.map(new BrowsePathsV2(dataMap)))); + mappingHelper.mapToResult(ACCESS_DATASET_ASPECT_NAME, ((dataset, dataMap) -> + dataset.setAccess(AccessMapper.map(new Access(dataMap), entityUrn)))); return mappingHelper.getResult(); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/VersionedDatasetMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/VersionedDatasetMapper.java index 5d9d40970ac43..241c4872b1caa 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/VersionedDatasetMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/dataset/mappers/VersionedDatasetMapper.java @@ -75,7 +75,7 @@ public VersionedDataset apply(@Nonnull final EntityResponse entityResponse) { mappingHelper.mapToResult(EDITABLE_DATASET_PROPERTIES_ASPECT_NAME, this::mapEditableDatasetProperties); mappingHelper.mapToResult(VIEW_PROPERTIES_ASPECT_NAME, this::mapViewProperties); mappingHelper.mapToResult(INSTITUTIONAL_MEMORY_ASPECT_NAME, (dataset, dataMap) -> - dataset.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap)))); + dataset.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap), entityUrn))); mappingHelper.mapToResult(OWNERSHIP_ASPECT_NAME, (dataset, dataMap) -> dataset.setOwnership(OwnershipMapper.map(new Ownership(dataMap), entityUrn))); mappingHelper.mapToResult(STATUS_ASPECT_NAME, (dataset, dataMap) -> diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/domain/DomainMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/domain/DomainMapper.java index 98919ff1f4430..fe52b5eff718f 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/domain/DomainMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/domain/DomainMapper.java @@ -45,7 +45,7 @@ public static Domain map(final EntityResponse entityResponse) { final EnvelopedAspect envelopedInstitutionalMemory = aspects.get(Constants.INSTITUTIONAL_MEMORY_ASPECT_NAME); if (envelopedInstitutionalMemory != null) { - result.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(envelopedInstitutionalMemory.getValue().data()))); + result.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(envelopedInstitutionalMemory.getValue().data()), entityUrn)); } return result; diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/glossary/mappers/GlossaryTermMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/glossary/mappers/GlossaryTermMapper.java index 21a2671c6c6f5..c98177b458dea 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/glossary/mappers/GlossaryTermMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/glossary/mappers/GlossaryTermMapper.java @@ -60,7 +60,7 @@ public GlossaryTerm apply(@Nonnull final EntityResponse entityResponse) { mappingHelper.mapToResult(DEPRECATION_ASPECT_NAME, (glossaryTerm, dataMap) -> glossaryTerm.setDeprecation(DeprecationMapper.map(new Deprecation(dataMap)))); mappingHelper.mapToResult(INSTITUTIONAL_MEMORY_ASPECT_NAME, (dataset, dataMap) -> - dataset.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap)))); + dataset.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap), entityUrn))); // If there's no name property, resort to the legacy name computation. if (result.getGlossaryTermInfo() != null && result.getGlossaryTermInfo().getName() == null) { diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mlmodel/mappers/MLFeatureMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mlmodel/mappers/MLFeatureMapper.java index eb6551bafc726..c924384756ac8 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mlmodel/mappers/MLFeatureMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mlmodel/mappers/MLFeatureMapper.java @@ -69,7 +69,7 @@ public MLFeature apply(@Nonnull final EntityResponse entityResponse) { mlFeature.setOwnership(OwnershipMapper.map(new Ownership(dataMap), entityUrn))); mappingHelper.mapToResult(ML_FEATURE_PROPERTIES_ASPECT_NAME, this::mapMLFeatureProperties); mappingHelper.mapToResult(INSTITUTIONAL_MEMORY_ASPECT_NAME, (mlFeature, dataMap) -> - mlFeature.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap)))); + mlFeature.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap), entityUrn))); mappingHelper.mapToResult(STATUS_ASPECT_NAME, (mlFeature, dataMap) -> mlFeature.setStatus(StatusMapper.map(new Status(dataMap)))); mappingHelper.mapToResult(DEPRECATION_ASPECT_NAME, (mlFeature, dataMap) -> diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mlmodel/mappers/MLFeatureTableMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mlmodel/mappers/MLFeatureTableMapper.java index fa6b77d870757..f0e7cffe5578d 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mlmodel/mappers/MLFeatureTableMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mlmodel/mappers/MLFeatureTableMapper.java @@ -68,7 +68,7 @@ public MLFeatureTable apply(@Nonnull final EntityResponse entityResponse) { mappingHelper.mapToResult(ML_FEATURE_TABLE_KEY_ASPECT_NAME, this::mapMLFeatureTableKey); mappingHelper.mapToResult(ML_FEATURE_TABLE_PROPERTIES_ASPECT_NAME, (entity, dataMap) -> this.mapMLFeatureTableProperties(entity, dataMap, entityUrn)); mappingHelper.mapToResult(INSTITUTIONAL_MEMORY_ASPECT_NAME, (mlFeatureTable, dataMap) -> - mlFeatureTable.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap)))); + mlFeatureTable.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap), entityUrn))); mappingHelper.mapToResult(STATUS_ASPECT_NAME, (mlFeatureTable, dataMap) -> mlFeatureTable.setStatus(StatusMapper.map(new Status(dataMap)))); mappingHelper.mapToResult(DEPRECATION_ASPECT_NAME, (mlFeatureTable, dataMap) -> diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mlmodel/mappers/MLModelMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mlmodel/mappers/MLModelMapper.java index ef38abc03c501..414ba5d196d6b 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mlmodel/mappers/MLModelMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mlmodel/mappers/MLModelMapper.java @@ -101,7 +101,7 @@ public MLModel apply(@Nonnull final EntityResponse entityResponse) { mappingHelper.mapToResult(CAVEATS_AND_RECOMMENDATIONS_ASPECT_NAME, (mlModel, dataMap) -> mlModel.setCaveatsAndRecommendations(CaveatsAndRecommendationsMapper.map(new CaveatsAndRecommendations(dataMap)))); mappingHelper.mapToResult(INSTITUTIONAL_MEMORY_ASPECT_NAME, (mlModel, dataMap) -> - mlModel.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap)))); + mlModel.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap), entityUrn))); mappingHelper.mapToResult(SOURCE_CODE_ASPECT_NAME, this::mapSourceCode); mappingHelper.mapToResult(STATUS_ASPECT_NAME, (mlModel, dataMap) -> mlModel.setStatus(StatusMapper.map(new Status(dataMap)))); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mlmodel/mappers/MLPrimaryKeyMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mlmodel/mappers/MLPrimaryKeyMapper.java index aa79f7bed2444..533c0f60a930a 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mlmodel/mappers/MLPrimaryKeyMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mlmodel/mappers/MLPrimaryKeyMapper.java @@ -65,7 +65,7 @@ public MLPrimaryKey apply(@Nonnull final EntityResponse entityResponse) { mappingHelper.mapToResult(ML_PRIMARY_KEY_KEY_ASPECT_NAME, this::mapMLPrimaryKeyKey); mappingHelper.mapToResult(ML_PRIMARY_KEY_PROPERTIES_ASPECT_NAME, this::mapMLPrimaryKeyProperties); mappingHelper.mapToResult(INSTITUTIONAL_MEMORY_ASPECT_NAME, (mlPrimaryKey, dataMap) -> - mlPrimaryKey.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap)))); + mlPrimaryKey.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap), entityUrn))); mappingHelper.mapToResult(STATUS_ASPECT_NAME, (mlPrimaryKey, dataMap) -> mlPrimaryKey.setStatus(StatusMapper.map(new Status(dataMap)))); mappingHelper.mapToResult(DEPRECATION_ASPECT_NAME, (mlPrimaryKey, dataMap) -> diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/notebook/mappers/NotebookMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/notebook/mappers/NotebookMapper.java index d3b355c13c8f8..2b937c86c9779 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/notebook/mappers/NotebookMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/notebook/mappers/NotebookMapper.java @@ -74,7 +74,7 @@ public Notebook apply(EntityResponse response) { mappingHelper.mapToResult(GLOBAL_TAGS_ASPECT_NAME, (notebook, dataMap) -> notebook.setTags(GlobalTagsMapper.map(new GlobalTags(dataMap), entityUrn))); mappingHelper.mapToResult(INSTITUTIONAL_MEMORY_ASPECT_NAME, (notebook, dataMap) -> - notebook.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap)))); + notebook.setInstitutionalMemory(InstitutionalMemoryMapper.map(new InstitutionalMemory(dataMap), entityUrn))); mappingHelper.mapToResult(DOMAINS_ASPECT_NAME, this::mapDomains); mappingHelper.mapToResult(SUB_TYPES_ASPECT_NAME, this::mapSubTypes); mappingHelper.mapToResult(GLOSSARY_TERMS_ASPECT_NAME, (notebook, dataMap) -> diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/policy/DataHubPolicyMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/policy/DataHubPolicyMapper.java index 3d28446872a22..167e1615fc4cc 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/policy/DataHubPolicyMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/policy/DataHubPolicyMapper.java @@ -1,5 +1,6 @@ package com.linkedin.datahub.graphql.types.policy; +import com.linkedin.common.UrnArray; import com.linkedin.common.urn.Urn; import com.linkedin.data.DataMap; import com.linkedin.datahub.graphql.generated.ActorFilter; @@ -67,6 +68,11 @@ private ActorFilter mapActors(final DataHubActorFilter actorFilter) { result.setAllGroups(actorFilter.isAllGroups()); result.setAllUsers(actorFilter.isAllUsers()); result.setResourceOwners(actorFilter.isResourceOwners()); + // Change here is not executed at the moment - leaving it for the future + UrnArray resourceOwnersTypes = actorFilter.getResourceOwnersTypes(); + if (resourceOwnersTypes != null) { + result.setResourceOwnersTypes(resourceOwnersTypes.stream().map(Urn::toString).collect(Collectors.toList())); + } if (actorFilter.hasGroups()) { result.setGroups(actorFilter.getGroups().stream().map(Urn::toString).collect(Collectors.toList())); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/rolemetadata/RoleType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/rolemetadata/RoleType.java new file mode 100644 index 0000000000000..084c4d5033ad0 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/rolemetadata/RoleType.java @@ -0,0 +1,120 @@ +package com.linkedin.datahub.graphql.types.rolemetadata; + +import com.google.common.collect.ImmutableSet; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.AutoCompleteResults; +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.Role; +import com.linkedin.datahub.graphql.generated.SearchResults; +import com.linkedin.datahub.graphql.types.SearchableEntityType; +import com.linkedin.datahub.graphql.types.rolemetadata.mappers.RoleMapper; +import com.linkedin.datahub.graphql.types.mappers.AutoCompleteResultsMapper; +import com.linkedin.datahub.graphql.types.mappers.UrnSearchResultsMapper; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.client.EntityClient; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.query.AutoCompleteResult; +import com.linkedin.metadata.query.SearchFlags; +import com.linkedin.metadata.query.filter.Filter; +import com.linkedin.metadata.search.SearchResult; +import graphql.execution.DataFetcherResult; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +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; + +public class RoleType implements SearchableEntityType, + com.linkedin.datahub.graphql.types.EntityType { + + static final Set ASPECTS_TO_FETCH = ImmutableSet.of( + Constants.ROLE_KEY, + Constants.ROLE_PROPERTIES_ASPECT_NAME, + Constants.ROLE_ACTORS_ASPECT_NAME + ); + + private final EntityClient _entityClient; + + public RoleType(final EntityClient entityClient) { + _entityClient = entityClient; + } + + @Override + public EntityType type() { + return EntityType.ROLE; + } + + @Override + public Function getKeyProvider() { + return Entity::getUrn; + } + + @Override + public Class objectClass() { + return Role.class; + } + + @Override + public List> batchLoad(@Nonnull List urns, + @Nonnull QueryContext context) throws Exception { + final List externalRolesUrns = urns.stream() + .map(UrnUtils::getUrn) + .collect(Collectors.toList()); + + try { + final Map entities = _entityClient.batchGetV2( + Constants.ROLE_ENTITY_NAME, + new HashSet<>(externalRolesUrns), + ASPECTS_TO_FETCH, + context.getAuthentication()); + + final List gmsResults = new ArrayList<>(); + for (Urn urn : externalRolesUrns) { + gmsResults.add(entities.getOrDefault(urn, null)); + } + return gmsResults.stream() + .map(gmsResult -> + gmsResult == null ? null : DataFetcherResult.newResult() + .data(RoleMapper.map(gmsResult)) + .build() + ) + .collect(Collectors.toList()); + } catch (Exception e) { + throw new RuntimeException("Failed to batch load Role", e); + } + } + + @Override + public SearchResults search(@Nonnull String query, + @Nullable List filters, + int start, + int count, + @Nonnull final QueryContext context) throws Exception { + final SearchResult searchResult = _entityClient.search(Constants.ROLE_ENTITY_NAME, + query, Collections.emptyMap(), start, count, + context.getAuthentication(), new SearchFlags().setFulltext(true)); + return UrnSearchResultsMapper.map(searchResult); + } + + @Override + public AutoCompleteResults autoComplete(@Nonnull String query, + @Nullable String field, + @Nullable Filter filters, + int limit, + @Nonnull final QueryContext context) throws Exception { + final AutoCompleteResult result = _entityClient.autoComplete(Constants.ROLE_ENTITY_NAME, + query, filters, limit, context.getAuthentication()); + return AutoCompleteResultsMapper.map(result); + } + +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/rolemetadata/mappers/AccessMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/rolemetadata/mappers/AccessMapper.java new file mode 100644 index 0000000000000..cabace1a52441 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/rolemetadata/mappers/AccessMapper.java @@ -0,0 +1,41 @@ +package com.linkedin.datahub.graphql.types.rolemetadata.mappers; + + +import com.linkedin.common.urn.Urn; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.generated.Role; +import com.linkedin.datahub.graphql.generated.RoleAssociation; + +import javax.annotation.Nonnull; +import java.util.stream.Collectors; + +public class AccessMapper { + public static final AccessMapper INSTANCE = new AccessMapper(); + + public static com.linkedin.datahub.graphql.generated.Access map( + @Nonnull final com.linkedin.common.Access access, + @Nonnull final Urn entityUrn) { + return INSTANCE.apply(access, entityUrn); + } + + public com.linkedin.datahub.graphql.generated.Access apply( + @Nonnull final com.linkedin.common.Access access, + @Nonnull final Urn entityUrn) { + com.linkedin.datahub.graphql.generated.Access result = new com.linkedin.datahub.graphql.generated.Access(); + result.setRoles(access.getRoles().stream().map( + association -> this.mapRoleAssociation(association, entityUrn) + ).collect(Collectors.toList())); + return result; + } + + private RoleAssociation mapRoleAssociation(com.linkedin.common.RoleAssociation association, Urn entityUrn) { + RoleAssociation roleAssociation = new RoleAssociation(); + Role role = new Role(); + role.setType(EntityType.ROLE); + role.setUrn(association.getUrn().toString()); + roleAssociation.setRole(role); + roleAssociation.setAssociatedUrn(entityUrn.toString()); + return roleAssociation; + } + +} diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/rolemetadata/mappers/RoleMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/rolemetadata/mappers/RoleMapper.java new file mode 100644 index 0000000000000..3cb0ec942a457 --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/rolemetadata/mappers/RoleMapper.java @@ -0,0 +1,93 @@ +package com.linkedin.datahub.graphql.types.rolemetadata.mappers; + +import com.linkedin.common.urn.Urn; +import com.linkedin.datahub.graphql.generated.Actor; +import com.linkedin.datahub.graphql.generated.CorpUser; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.generated.Role; +import com.linkedin.datahub.graphql.generated.RoleProperties; +import com.linkedin.datahub.graphql.generated.RoleUser; +import com.linkedin.datahub.graphql.types.mappers.ModelMapper; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.EnvelopedAspect; +import com.linkedin.entity.EnvelopedAspectMap; +import com.linkedin.metadata.Constants; +import com.linkedin.metadata.key.RoleKey; +import com.linkedin.role.Actors; +import com.linkedin.role.RoleUserArray; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.stream.Collectors; + +public class RoleMapper implements ModelMapper { + + public static final RoleMapper INSTANCE = new RoleMapper(); + + public static Role map(@Nonnull final EntityResponse entityResponse) { + return INSTANCE.apply(entityResponse); + } + + private static RoleProperties mapRoleProperties(final com.linkedin.role.RoleProperties e) { + final RoleProperties propertiesResult = new RoleProperties(); + propertiesResult.setName(e.getName()); + propertiesResult.setDescription(e.getDescription()); + propertiesResult.setType(e.getType()); + propertiesResult.setRequestUrl(e.getRequestUrl()); + + return propertiesResult; + } + + private static RoleUser mapCorpUsers(final com.linkedin.role.RoleUser provisionedUser) { + RoleUser result = new RoleUser(); + CorpUser corpUser = new CorpUser(); + corpUser.setUrn(provisionedUser.getUser().toString()); + result.setUser(corpUser); + return result; + } + + private static Actor mapActor(Actors actors) { + Actor actor = new Actor(); + actor.setUsers(mapRoleUsers(actors.getUsers())); + return actor; + } + + private static List mapRoleUsers(RoleUserArray users) { + if (users == null) { + return null; + } + return users.stream().map(x -> mapCorpUsers(x)).collect(Collectors.toList()); + } + + @Override + public Role apply(EntityResponse input) { + + + final Role result = new Role(); + final Urn entityUrn = input.getUrn(); + + result.setUrn(entityUrn.toString()); + result.setType(EntityType.ROLE); + + final EnvelopedAspectMap aspects = input.getAspects(); + + final EnvelopedAspect roleKeyAspect = aspects.get(Constants.ROLE_KEY); + if (roleKeyAspect != null) { + result.setId(new RoleKey(roleKeyAspect.getValue().data()).getId()); + } + final EnvelopedAspect envelopedPropertiesAspect = aspects.get(Constants.ROLE_PROPERTIES_ASPECT_NAME); + if (envelopedPropertiesAspect != null) { + result.setProperties(mapRoleProperties( + new com.linkedin.role.RoleProperties( + envelopedPropertiesAspect.getValue().data())) + ); + } + + final EnvelopedAspect envelopedUsers = aspects.get(Constants.ROLE_ACTORS_ASPECT_NAME); + if (envelopedUsers != null) { + result.setActors(mapActor(new Actors(envelopedUsers.getValue().data()))); + } + + return result; + } +} diff --git a/datahub-graphql-core/src/main/resources/app.graphql b/datahub-graphql-core/src/main/resources/app.graphql index 1d077d4be212b..37183bac13f0e 100644 --- a/datahub-graphql-core/src/main/resources/app.graphql +++ b/datahub-graphql-core/src/main/resources/app.graphql @@ -211,6 +211,11 @@ type VisualConfig { Configuration for the queries tab """ queriesTab: QueriesTabConfig + + """ + Configuration for the queries tab + """ + entityProfiles: EntityProfilesConfig } """ @@ -223,6 +228,28 @@ type QueriesTabConfig { queriesTabResultSize: Int } + +""" +Configuration for different entity profiles +""" +type EntityProfilesConfig { + """ + The configurations for a Domain entity profile + """ + domain: EntityProfileConfig +} + +""" +Configuration for an entity profile +""" +type EntityProfileConfig { + """ + The enum value from EntityProfileTab for which tab should be showed by default on + entity profile pages. If null, rely on default sorting from React code. + """ + defaultTab: String +} + """ Configurations related to tracking users in the app """ @@ -389,6 +416,11 @@ type FeatureFlagsConfig { Whether browse V2 sidebar should be shown """ showBrowseV2: Boolean! + + """ + Whether we should show CTAs in the UI related to moving to Managed DataHub by Acryl. + """ + showAcrylInfo: Boolean! } """ diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index 2dff8c0f27926..59e35d5bc3ff4 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -886,6 +886,11 @@ enum EntityType { A Custom Ownership Type """ CUSTOM_OWNERSHIP_TYPE + + """" + A Role from an organisation + """ + ROLE } """ @@ -1260,6 +1265,11 @@ type Dataset implements EntityWithRelationships & Entity & BrowsableEntity { """ domain: DomainAssociation + """ + The Roles and the properties to access the dataset + """ + access: Access + """ Statistics about how this Dataset is used The first parameter, `resource`, is deprecated and no longer needs to be provided @@ -1382,7 +1392,9 @@ type Dataset implements EntityWithRelationships & Entity & BrowsableEntity { siblings: SiblingProperties """ - fine grained lineage + Lineage information for the column-level. Includes a list of objects + detailing which columns are upstream and which are downstream of each other. + The upstream and downstream columns are from datasets. """ fineGrainedLineages: [FineGrainedLineage!] @@ -1397,6 +1409,94 @@ type Dataset implements EntityWithRelationships & Entity & BrowsableEntity { exists: Boolean } +type RoleAssociation { + + """ + The Role entity itself + """ + role: Role! + + """ + Reference back to the tagged urn for tracking purposes e.g. when sibling nodes are merged together + """ + associatedUrn: String! + +} + +type Access { + roles: [RoleAssociation!] +} + +type Role implements Entity { + """ + A primary key of the Metadata Entity + """ + urn: String! + + """ + A standard Entity Type + """ + type: EntityType! + + """ + List of relationships between the source Entity and some destination entities with a given types + """ + relationships(input: RelationshipsInput!): EntityRelationshipsResult + + """ + Id of the Role + """ + id: String! + + """ + Role properties to include Request Access Url + """ + properties: RoleProperties! + + """ + A standard Entity Type + """ + actors: Actor! + + +} + +type Actor { + """ + List of users for which the role is provisioned + """ + users: [RoleUser!] +} + +type RoleUser { + """ + Linked corp user of a role + """ + user: CorpUser! +} + +type RoleProperties { + """ + Name of the Role in an organisation + """ + name: String! + + """ + Description about the role + """ + description: String + + """ + Role type can be READ, WRITE or ADMIN + """ + type: String + + """ + Url to request a role for a user in an organisation + """ + requestUrl: String +} + type FineGrainedLineage { upstreams: [SchemaFieldRef!] downstreams: [SchemaFieldRef!] @@ -2483,6 +2583,11 @@ type InstitutionalMemoryMetadata { Description of the resource """ description: String! @deprecated + + """ + Reference back to the owned urn for tracking purposes e.g. when sibling nodes are merged together + """ + associatedUrn: String! } """ @@ -5648,11 +5753,9 @@ type DataJob implements EntityWithRelationships & Entity & BrowsableEntity { info: DataJobInfo @deprecated """ - Deprecated, use relationship Produces, Consumes, DownstreamOf instead - Information about the inputs and outputs of a Data processing job + Information about the inputs and outputs of a Data processing job including column-level lineage. """ - inputOutput: DataJobInputOutput @deprecated - + inputOutput: DataJobInputOutput """ Deprecated, use the tags field instead @@ -5877,6 +5980,13 @@ type DataJobInputOutput { Input datajobs that this data job depends on """ inputDatajobs: [DataJob!] @deprecated + + """ + Lineage information for the column-level. Includes a list of objects + detailing which columns are upstream and which are downstream of each other. + The upstream and downstream columns are from datasets. + """ + fineGrainedLineages: [FineGrainedLineage!] } """ @@ -7995,6 +8105,16 @@ type ActorFilter { """ resourceOwners: Boolean! + """ + Set of OwnershipTypes to apply the policy to (if resourceOwners field is set to True) + """ + resourceOwnersTypes: [String!] + + """ + Set of OwnershipTypes to apply the policy to (if resourceOwners field is set to True), resolved. + """ + resolvedOwnershipTypes: [OwnershipTypeEntity!] + """ Whether the filter should apply to all users """ @@ -8123,6 +8243,11 @@ input ActorFilterInput { """ resourceOwners: Boolean! + """ + Set of OwnershipTypes to apply the policy to (if resourceOwners field is set to True) + """ + resourceOwnersTypes: [String!] + """ Whether the filter should apply to all users """ diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/glossary/CreateGlossaryTermResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/glossary/CreateGlossaryTermResolverTest.java index 929162db1cc43..e4f32133b4b51 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/glossary/CreateGlossaryTermResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/glossary/CreateGlossaryTermResolverTest.java @@ -2,24 +2,40 @@ import com.datahub.authentication.Authentication; import com.linkedin.common.urn.GlossaryNodeUrn; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.generated.CreateGlossaryEntityInput; +import com.linkedin.entity.Aspect; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.EnvelopedAspect; +import com.linkedin.entity.EnvelopedAspectMap; import com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils; import com.linkedin.entity.client.EntityClient; import com.linkedin.glossary.GlossaryTermInfo; import com.linkedin.metadata.key.GlossaryTermKey; +import com.linkedin.metadata.search.SearchEntity; +import com.linkedin.metadata.search.SearchEntityArray; +import com.linkedin.metadata.search.SearchResult; import com.linkedin.metadata.entity.EntityService; import com.linkedin.mxe.MetadataChangeProposal; import graphql.schema.DataFetchingEnvironment; import org.mockito.Mockito; import org.testng.annotations.Test; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletionException; + import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext; +import static org.testng.Assert.assertThrows; import static com.linkedin.metadata.Constants.*; - public class CreateGlossaryTermResolverTest { + private static final String EXISTING_TERM_URN = "urn:li:glossaryTerm:testing12345"; + private static final CreateGlossaryEntityInput TEST_INPUT = new CreateGlossaryEntityInput( "test-id", "test-name", @@ -69,7 +85,7 @@ private MetadataChangeProposal setupTest( @Test public void testGetSuccess() throws Exception { - EntityClient mockClient = Mockito.mock(EntityClient.class); + EntityClient mockClient = initMockClient(); EntityService mockService = Mockito.mock(EntityService.class); DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); final MetadataChangeProposal proposal = setupTest(mockEnv, TEST_INPUT, "test-description", parentNodeUrn); @@ -86,7 +102,7 @@ public void testGetSuccess() throws Exception { @Test public void testGetSuccessNoDescription() throws Exception { - EntityClient mockClient = Mockito.mock(EntityClient.class); + EntityClient mockClient = initMockClient(); EntityService mockService = Mockito.mock(EntityService.class); DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); final MetadataChangeProposal proposal = setupTest(mockEnv, TEST_INPUT_NO_DESCRIPTION, "", parentNodeUrn); @@ -103,7 +119,7 @@ public void testGetSuccessNoDescription() throws Exception { @Test public void testGetSuccessNoParentNode() throws Exception { - EntityClient mockClient = Mockito.mock(EntityClient.class); + EntityClient mockClient = initMockClient(); EntityService mockService = Mockito.mock(EntityService.class); DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); final MetadataChangeProposal proposal = setupTest(mockEnv, TEST_INPUT_NO_PARENT_NODE, "test-description", null); @@ -117,4 +133,80 @@ public void testGetSuccessNoParentNode() throws Exception { Mockito.eq(false) ); } + + @Test + public void testGetFailureExistingTermSameName() throws Exception { + EntityClient mockClient = Mockito.mock(EntityClient.class); + + Mockito.when( + mockClient.filter( + Mockito.eq(GLOSSARY_TERM_ENTITY_NAME), + Mockito.any(), + Mockito.eq(null), + Mockito.eq(0), + Mockito.eq(1000), + Mockito.any() + ) + ).thenReturn(new SearchResult().setEntities( + new SearchEntityArray(new SearchEntity().setEntity(UrnUtils.getUrn(EXISTING_TERM_URN))) + )); + + Map result = new HashMap<>(); + EnvelopedAspectMap map = new EnvelopedAspectMap(); + GlossaryTermInfo termInfo = new GlossaryTermInfo().setName("Duplicated Name"); + map.put(GLOSSARY_TERM_INFO_ASPECT_NAME, new EnvelopedAspect().setValue(new Aspect(termInfo.data()))); + result.put(UrnUtils.getUrn(EXISTING_TERM_URN), new EntityResponse().setAspects(map)); + + Mockito.when( + mockClient.batchGetV2( + Mockito.eq(GLOSSARY_TERM_ENTITY_NAME), + Mockito.any(), + Mockito.eq(Collections.singleton(GLOSSARY_TERM_INFO_ASPECT_NAME)), + Mockito.any() + ) + ).thenReturn(result); + + EntityService mockService = Mockito.mock(EntityService.class); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + + CreateGlossaryEntityInput input = new CreateGlossaryEntityInput( + "test-id", + "Duplicated Name", + "test-description", + "urn:li:glossaryNode:12372c2ec7754c308993202dc44f548b" + ); + setupTest(mockEnv, input, "test-description", parentNodeUrn); + CreateGlossaryTermResolver resolver = new CreateGlossaryTermResolver(mockClient, mockService); + assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join()); + + Mockito.verify(mockClient, Mockito.times(0)).ingestProposal( + Mockito.any(), + Mockito.any(Authentication.class) + ); + } + + private EntityClient initMockClient() throws Exception { + EntityClient mockClient = Mockito.mock(EntityClient.class); + + Mockito.when( + mockClient.filter( + Mockito.eq(GLOSSARY_TERM_ENTITY_NAME), + Mockito.any(), + Mockito.eq(null), + Mockito.eq(0), + Mockito.eq(1000), + Mockito.any() + ) + ).thenReturn(new SearchResult().setEntities(new SearchEntityArray())); + Mockito.when( + mockClient.batchGetV2( + Mockito.eq(GLOSSARY_TERM_ENTITY_NAME), + Mockito.any(), + Mockito.eq(Collections.singleton(GLOSSARY_TERM_INFO_ASPECT_NAME)), + Mockito.any() + ) + ).thenReturn(new HashMap<>()); + + return mockClient; + } } diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/ingest/execution/CreateIngestionExecutionRequestResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/ingest/execution/CreateIngestionExecutionRequestResolverTest.java index 35043fa0879f3..7973e49c6efdf 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/ingest/execution/CreateIngestionExecutionRequestResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/ingest/execution/CreateIngestionExecutionRequestResolverTest.java @@ -1,6 +1,7 @@ package com.linkedin.datahub.graphql.resolvers.ingest.execution; import com.datahub.authentication.Authentication; +import com.linkedin.metadata.config.IngestionConfiguration; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.linkedin.datahub.graphql.QueryContext; @@ -11,7 +12,6 @@ import com.linkedin.entity.EnvelopedAspectMap; import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.Constants; -import com.linkedin.metadata.config.IngestionConfiguration; import com.linkedin.mxe.MetadataChangeProposal; import com.linkedin.r2.RemoteInvocationException; import graphql.schema.DataFetchingEnvironment; diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/ingest/execution/CreateTestConnectionRequestResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/ingest/execution/CreateTestConnectionRequestResolverTest.java index 0eb9366f0493b..75df240441965 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/ingest/execution/CreateTestConnectionRequestResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/ingest/execution/CreateTestConnectionRequestResolverTest.java @@ -1,10 +1,10 @@ package com.linkedin.datahub.graphql.resolvers.ingest.execution; import com.datahub.authentication.Authentication; +import com.linkedin.metadata.config.IngestionConfiguration; import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.generated.CreateTestConnectionRequestInput; import com.linkedin.entity.client.EntityClient; -import com.linkedin.metadata.config.IngestionConfiguration; import com.linkedin.mxe.MetadataChangeProposal; import graphql.schema.DataFetchingEnvironment; import org.mockito.Mockito; diff --git a/datahub-upgrade/build.gradle b/datahub-upgrade/build.gradle index 679e54871cbc8..ad2bf02bfdcc7 100644 --- a/datahub-upgrade/build.gradle +++ b/datahub-upgrade/build.gradle @@ -15,6 +15,7 @@ dependencies { compile project(':metadata-io') compile project(':metadata-service:factories') compile project(':metadata-service:restli-client') + compile project(':metadata-service:configuration') implementation externalDependency.charle compile externalDependency.javaxInject diff --git a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/nocode/DataMigrationStep.java b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/nocode/DataMigrationStep.java index 689b1fb997f38..6553bb80bb1fa 100644 --- a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/nocode/DataMigrationStep.java +++ b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/nocode/DataMigrationStep.java @@ -9,11 +9,11 @@ import com.linkedin.datahub.upgrade.UpgradeStep; import com.linkedin.datahub.upgrade.UpgradeStepResult; import com.linkedin.metadata.Constants; +import com.linkedin.metadata.entity.EntityService; import com.linkedin.metadata.models.AspectSpec; import com.linkedin.metadata.models.registry.EntityRegistry; import com.linkedin.metadata.utils.PegasusUtils; import com.datahub.util.RecordUtils; -import com.linkedin.metadata.entity.EntityService; import com.linkedin.metadata.entity.ebean.EbeanAspectV1; import com.linkedin.metadata.entity.ebean.EbeanAspectV2; import com.linkedin.metadata.models.EntitySpec; diff --git a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/nocode/NoCodeUpgrade.java b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/nocode/NoCodeUpgrade.java index d1715ce7d66a6..c12ff201faf22 100644 --- a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/nocode/NoCodeUpgrade.java +++ b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/nocode/NoCodeUpgrade.java @@ -33,8 +33,7 @@ public NoCodeUpgrade( final Authentication systemAuthentication, final RestliEntityClient entityClient) { _steps = buildUpgradeSteps( - server, - entityService, + server, entityService, entityRegistry, systemAuthentication, entityClient); diff --git a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/elasticsearch/steps/CleanIndicesStep.java b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/elasticsearch/steps/CleanIndicesStep.java index 99c5e7444d16b..f60aa283c0140 100644 --- a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/elasticsearch/steps/CleanIndicesStep.java +++ b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/system/elasticsearch/steps/CleanIndicesStep.java @@ -1,11 +1,11 @@ package com.linkedin.datahub.upgrade.system.elasticsearch.steps; +import com.linkedin.metadata.config.search.ElasticSearchConfiguration; import com.linkedin.datahub.upgrade.UpgradeContext; import com.linkedin.datahub.upgrade.UpgradeStep; import com.linkedin.datahub.upgrade.UpgradeStepResult; import com.linkedin.datahub.upgrade.impl.DefaultUpgradeStepResult; import com.linkedin.datahub.upgrade.system.elasticsearch.util.IndexUtils; -import com.linkedin.metadata.config.search.ElasticSearchConfiguration; import com.linkedin.metadata.search.elasticsearch.indexbuilder.ESIndexBuilder; import com.linkedin.metadata.shared.ElasticSearchIndexed; import lombok.extern.slf4j.Slf4j; diff --git a/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/DatahubUpgradeNoSchemaRegistryTest.java b/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/DatahubUpgradeNoSchemaRegistryTest.java index dd47e049dd187..db697a40d0c6c 100644 --- a/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/DatahubUpgradeNoSchemaRegistryTest.java +++ b/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/DatahubUpgradeNoSchemaRegistryTest.java @@ -19,7 +19,8 @@ @ActiveProfiles("test") @SpringBootTest(classes = {UpgradeCliApplication.class, UpgradeCliApplicationTestConfiguration.class}, properties = { - "kafka.schemaRegistry.type=INTERNAL" + "kafka.schemaRegistry.type=INTERNAL", + "DATAHUB_UPGRADE_HISTORY_TOPIC_NAME=test_due_topic" }) public class DatahubUpgradeNoSchemaRegistryTest extends AbstractTestNGSpringContextTests { diff --git a/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/UpgradeCliApplicationTestConfiguration.java b/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/UpgradeCliApplicationTestConfiguration.java index 8ae3a832d0aaf..fefc853be8c0b 100644 --- a/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/UpgradeCliApplicationTestConfiguration.java +++ b/datahub-upgrade/src/test/java/com/linkedin/datahub/upgrade/UpgradeCliApplicationTestConfiguration.java @@ -22,7 +22,7 @@ public class UpgradeCliApplicationTestConfiguration { private EbeanServer ebeanServer; @MockBean - private EntityService entityService; + private EntityService _entityService; @MockBean private SearchService searchService; diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index 8302fb09af4af..be853361574ab 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -230,6 +230,7 @@ export const dataset1 = { actor: 'urn:li:corpuser:1', time: 1612396473001, }, + associatedUrn: 'urn:li:dataset:1', }, ], }, @@ -472,6 +473,7 @@ export const dataset3 = { actor: 'urn:li:corpuser:1', time: 1612396473001, }, + associatedUrn: 'urn:li:dataset:3', }, ], }, diff --git a/datahub-web-react/src/app/analytics/event.ts b/datahub-web-react/src/app/analytics/event.ts index 9cb14f5aa127b..84173b522fb07 100644 --- a/datahub-web-react/src/app/analytics/event.ts +++ b/datahub-web-react/src/app/analytics/event.ts @@ -23,6 +23,7 @@ export enum EventType { BrowseV2ToggleSidebarEvent, BrowseV2ToggleNodeEvent, BrowseV2SelectNodeEvent, + BrowseV2EntityLinkClickEvent, EntityViewEvent, EntitySectionViewEvent, EntityActionEvent, @@ -244,7 +245,7 @@ export interface BrowseV2ToggleNodeEvent extends BaseEvent { */ export interface BrowseV2SelectNodeEvent extends BaseEvent { type: EventType.BrowseV2SelectNodeEvent; - targetNode: 'browse'; + targetNode: 'browse' | 'platform'; action: 'select' | 'deselect'; entity: string; environment?: string; @@ -252,6 +253,18 @@ export interface BrowseV2SelectNodeEvent extends BaseEvent { targetDepth: number; } +/** + * Logged when a user clicks a container link in the sidebar + */ +export interface BrowseV2EntityLinkClickEvent extends BaseEvent { + type: EventType.BrowseV2EntityLinkClickEvent; + targetNode: 'browse'; + entity: string; + environment?: string; + platform?: string; + targetDepth: number; +} + /** * Logged when user views an entity profile. */ @@ -613,6 +626,7 @@ export type Event = | BrowseV2ToggleSidebarEvent | BrowseV2ToggleNodeEvent | BrowseV2SelectNodeEvent + | BrowseV2EntityLinkClickEvent | EntityViewEvent | EntitySectionViewEvent | EntityActionEvent diff --git a/datahub-web-react/src/app/analyticsDashboard/components/TimeSeriesChart.tsx b/datahub-web-react/src/app/analyticsDashboard/components/TimeSeriesChart.tsx index 796f68888caa0..6b9b808abfd0f 100644 --- a/datahub-web-react/src/app/analyticsDashboard/components/TimeSeriesChart.tsx +++ b/datahub-web-react/src/app/analyticsDashboard/components/TimeSeriesChart.tsx @@ -7,6 +7,15 @@ import Legend from './Legend'; import { addInterval } from '../../shared/time/timeUtils'; import { formatNumber } from '../../shared/formatNumber'; +type ScaleConfig = { + type: 'time' | 'timeUtc' | 'linear' | 'band' | 'ordinal'; + includeZero?: boolean; +}; + +type AxisConfig = { + formatter: (tick: number) => string; +}; + type Props = { chartData: TimeSeriesChartType; width: number; @@ -20,9 +29,16 @@ type Props = { crossHairLineColor?: string; }; insertBlankPoints?: boolean; + yScale?: ScaleConfig; + yAxis?: AxisConfig; }; -const MARGIN_SIZE = 40; +const MARGIN = { + TOP: 40, + RIGHT: 45, + BOTTOM: 40, + LEFT: 40, +}; function insertBlankAt(ts: number, newLine: Array) { const dateString = new Date(ts).toISOString(); @@ -60,7 +76,16 @@ export function computeLines(chartData: TimeSeriesChartType, insertBlankPoints: return returnLines; } -export const TimeSeriesChart = ({ chartData, width, height, hideLegend, style, insertBlankPoints }: Props) => { +export const TimeSeriesChart = ({ + chartData, + width, + height, + hideLegend, + style, + insertBlankPoints, + yScale, + yAxis, +}: Props) => { const ordinalColorScale = scaleOrdinal({ domain: chartData.lines.map((data) => data.name), range: lineColors.slice(0, chartData.lines.length), @@ -75,9 +100,13 @@ export const TimeSeriesChart = ({ chartData, width, height, hideLegend, style, i ariaLabel={chartData.title} width={width} height={height} - margin={{ top: MARGIN_SIZE, right: MARGIN_SIZE, bottom: MARGIN_SIZE, left: MARGIN_SIZE }} + margin={{ top: MARGIN.TOP, right: MARGIN.RIGHT, bottom: MARGIN.BOTTOM, left: MARGIN.LEFT }} xScale={{ type: 'time' }} - yScale={{ type: 'linear' }} + yScale={ + yScale ?? { + type: 'linear', + } + } renderTooltip={({ datum }) => (
{new Date(Number(datum.x)).toDateString()}
@@ -89,7 +118,7 @@ export const TimeSeriesChart = ({ chartData, width, height, hideLegend, style, i formatNumber(tick)} + tickFormat={(tick) => (yAxis?.formatter ? yAxis.formatter(tick) : formatNumber(tick))} /> {lines.map((line, i) => ( { name: 'Preview', component: EmbedTab, display: { - visible: (_, chart: GetChartQuery) => !!chart?.chart?.embed?.renderUrl, - enabled: (_, chart: GetChartQuery) => !!chart?.chart?.embed?.renderUrl, + visible: (_, chart: GetChartQuery) => + !!chart?.chart?.embed?.renderUrl && chart?.chart?.platform.urn === LOOKER_URN, + enabled: (_, chart: GetChartQuery) => + !!chart?.chart?.embed?.renderUrl && chart?.chart?.platform.urn === LOOKER_URN, }, }, { @@ -127,6 +130,9 @@ export class ChartEntity implements Entity { { component: SidebarAboutSection, }, + { + component: SidebarOwnerSection, + }, { component: SidebarTagsSection, properties: { @@ -134,9 +140,6 @@ export class ChartEntity implements Entity { hasTerms: true, }, }, - { - component: SidebarOwnerSection, - }, { component: SidebarDomainSection, }, diff --git a/datahub-web-react/src/app/entity/container/ContainerEntity.tsx b/datahub-web-react/src/app/entity/container/ContainerEntity.tsx index d48ef8f1db595..201dcb9e4487a 100644 --- a/datahub-web-react/src/app/entity/container/ContainerEntity.tsx +++ b/datahub-web-react/src/app/entity/container/ContainerEntity.tsx @@ -88,6 +88,9 @@ export class ContainerEntity implements Entity { { component: SidebarAboutSection, }, + { + component: SidebarOwnerSection, + }, { component: SidebarTagsSection, properties: { @@ -95,9 +98,6 @@ export class ContainerEntity implements Entity { hasTerms: true, }, }, - { - component: SidebarOwnerSection, - }, { component: SidebarDomainSection, }, diff --git a/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx b/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx index b7c390dad9019..d948b21a46262 100644 --- a/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx +++ b/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx @@ -29,6 +29,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 { LOOKER_URN } from '../../ingest/source/builder/constants'; /** * Definition of the DataHub Dashboard entity. @@ -113,8 +114,12 @@ export class DashboardEntity implements Entity { name: 'Preview', component: EmbedTab, display: { - visible: (_, dashboard: GetDashboardQuery) => !!dashboard?.dashboard?.embed?.renderUrl, - enabled: (_, dashboard: GetDashboardQuery) => !!dashboard?.dashboard?.embed?.renderUrl, + visible: (_, dashboard: GetDashboardQuery) => + !!dashboard?.dashboard?.embed?.renderUrl && + dashboard?.dashboard?.platform.urn === LOOKER_URN, + enabled: (_, dashboard: GetDashboardQuery) => + !!dashboard?.dashboard?.embed?.renderUrl && + dashboard?.dashboard?.platform.urn === LOOKER_URN, }, }, { @@ -134,16 +139,16 @@ export class DashboardEntity implements Entity { component: SidebarAboutSection, }, { - component: SidebarTagsSection, + component: SidebarOwnerSection, properties: { - hasTags: true, - hasTerms: true, + defaultOwnerType: OwnershipType.TechnicalOwner, }, }, { - component: SidebarOwnerSection, + component: SidebarTagsSection, properties: { - defaultOwnerType: OwnershipType.TechnicalOwner, + hasTags: true, + hasTerms: true, }, }, { diff --git a/datahub-web-react/src/app/entity/dataFlow/DataFlowEntity.tsx b/datahub-web-react/src/app/entity/dataFlow/DataFlowEntity.tsx index 3f2b1a6dfd273..c6f7c8b6a6cf7 100644 --- a/datahub-web-react/src/app/entity/dataFlow/DataFlowEntity.tsx +++ b/datahub-web-react/src/app/entity/dataFlow/DataFlowEntity.tsx @@ -85,16 +85,16 @@ export class DataFlowEntity implements Entity { component: SidebarAboutSection, }, { - component: SidebarTagsSection, + component: SidebarOwnerSection, properties: { - hasTags: true, - hasTerms: true, + defaultOwnerType: OwnershipType.TechnicalOwner, }, }, { - component: SidebarOwnerSection, + component: SidebarTagsSection, properties: { - defaultOwnerType: OwnershipType.TechnicalOwner, + hasTags: true, + hasTerms: true, }, }, { diff --git a/datahub-web-react/src/app/entity/dataJob/DataJobEntity.tsx b/datahub-web-react/src/app/entity/dataJob/DataJobEntity.tsx index 398e79688a2ca..a2a369ec53ecf 100644 --- a/datahub-web-react/src/app/entity/dataJob/DataJobEntity.tsx +++ b/datahub-web-react/src/app/entity/dataJob/DataJobEntity.tsx @@ -108,16 +108,16 @@ export class DataJobEntity implements Entity { component: SidebarAboutSection, }, { - component: SidebarTagsSection, + component: SidebarOwnerSection, properties: { - hasTags: true, - hasTerms: true, + defaultOwnerType: OwnershipType.TechnicalOwner, }, }, { - component: SidebarOwnerSection, + component: SidebarTagsSection, properties: { - defaultOwnerType: OwnershipType.TechnicalOwner, + hasTags: true, + hasTerms: true, }, }, { diff --git a/datahub-web-react/src/app/entity/dataset/profile/stats/historical/HistoricalStatsView.tsx b/datahub-web-react/src/app/entity/dataset/profile/stats/historical/HistoricalStatsView.tsx index 7a28020291a22..4b256ab5eee01 100644 --- a/datahub-web-react/src/app/entity/dataset/profile/stats/historical/HistoricalStatsView.tsx +++ b/datahub-web-react/src/app/entity/dataset/profile/stats/historical/HistoricalStatsView.tsx @@ -104,7 +104,7 @@ const LOOKBACK_WINDOWS = [ { text: '1 year', windowSize: { interval: DateInterval.Year, count: 1 } }, ]; -const DEFAULT_LOOKBACK_WINDOW = '1 week'; +const DEFAULT_LOOKBACK_WINDOW = '3 months'; const getLookbackWindowSize = (text: string) => { for (let i = 0; i < LOOKBACK_WINDOWS.length; i++) { diff --git a/datahub-web-react/src/app/entity/domain/DomainEntity.tsx b/datahub-web-react/src/app/entity/domain/DomainEntity.tsx index e812ee87e6887..3b3045abe2a7c 100644 --- a/datahub-web-react/src/app/entity/domain/DomainEntity.tsx +++ b/datahub-web-react/src/app/entity/domain/DomainEntity.tsx @@ -13,6 +13,7 @@ import { DomainEntitiesTab } from './DomainEntitiesTab'; import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown'; import { EntityActionItem } from '../shared/entity/EntityActions'; import DataProductsTab from './DataProductsTab/DataProductsTab'; +import { EntityProfileTab } from '../shared/constants'; // import { EntityActionItem } from '../shared/entity/EntityActions'; /** @@ -72,14 +73,17 @@ export class DomainEntity implements Entity { isNameEditable tabs={[ { + id: EntityProfileTab.DOMAIN_ENTITIES_TAB, name: 'Entities', component: DomainEntitiesTab, }, { + id: EntityProfileTab.DOCUMENTATION_TAB, name: 'Documentation', component: DocumentationTab, }, { + id: EntityProfileTab.DATA_PRODUCTS_TAB, name: 'Data Products', component: DataProductsTab, }, diff --git a/datahub-web-react/src/app/entity/mlFeature/MLFeatureEntity.tsx b/datahub-web-react/src/app/entity/mlFeature/MLFeatureEntity.tsx index 4e6ea985d76dc..8fddae7c15186 100644 --- a/datahub-web-react/src/app/entity/mlFeature/MLFeatureEntity.tsx +++ b/datahub-web-react/src/app/entity/mlFeature/MLFeatureEntity.tsx @@ -91,16 +91,16 @@ export class MLFeatureEntity implements Entity { component: SidebarAboutSection, }, { - component: SidebarTagsSection, + component: SidebarOwnerSection, properties: { - hasTags: true, - hasTerms: true, + defaultOwnerType: OwnershipType.TechnicalOwner, }, }, { - component: SidebarOwnerSection, + component: SidebarTagsSection, properties: { - defaultOwnerType: OwnershipType.TechnicalOwner, + hasTags: true, + hasTerms: true, }, }, { diff --git a/datahub-web-react/src/app/entity/mlFeatureTable/MLFeatureTableEntity.tsx b/datahub-web-react/src/app/entity/mlFeatureTable/MLFeatureTableEntity.tsx index 52ea176e07807..3bb54b739e749 100644 --- a/datahub-web-react/src/app/entity/mlFeatureTable/MLFeatureTableEntity.tsx +++ b/datahub-web-react/src/app/entity/mlFeatureTable/MLFeatureTableEntity.tsx @@ -94,16 +94,16 @@ export class MLFeatureTableEntity implements Entity { component: SidebarAboutSection, }, { - component: SidebarTagsSection, + component: SidebarOwnerSection, properties: { - hasTags: true, - hasTerms: true, + defaultOwnerType: OwnershipType.TechnicalOwner, }, }, { - component: SidebarOwnerSection, + component: SidebarTagsSection, properties: { - defaultOwnerType: OwnershipType.TechnicalOwner, + hasTags: true, + hasTerms: true, }, }, { diff --git a/datahub-web-react/src/app/entity/mlModel/MLModelEntity.tsx b/datahub-web-react/src/app/entity/mlModel/MLModelEntity.tsx index 94cd9c6d87ad4..3e800f4f733d2 100644 --- a/datahub-web-react/src/app/entity/mlModel/MLModelEntity.tsx +++ b/datahub-web-react/src/app/entity/mlModel/MLModelEntity.tsx @@ -99,16 +99,16 @@ export class MLModelEntity implements Entity { component: SidebarAboutSection, }, { - component: SidebarTagsSection, + component: SidebarOwnerSection, properties: { - hasTags: true, - hasTerms: true, + defaultOwnerType: OwnershipType.TechnicalOwner, }, }, { - component: SidebarOwnerSection, + component: SidebarTagsSection, properties: { - defaultOwnerType: OwnershipType.TechnicalOwner, + hasTags: true, + hasTerms: true, }, }, { diff --git a/datahub-web-react/src/app/entity/mlModelGroup/MLModelGroupEntity.tsx b/datahub-web-react/src/app/entity/mlModelGroup/MLModelGroupEntity.tsx index 4d19a106bc570..1282eab47cefc 100644 --- a/datahub-web-react/src/app/entity/mlModelGroup/MLModelGroupEntity.tsx +++ b/datahub-web-react/src/app/entity/mlModelGroup/MLModelGroupEntity.tsx @@ -82,16 +82,16 @@ export class MLModelGroupEntity implements Entity { component: SidebarAboutSection, }, { - component: SidebarTagsSection, + component: SidebarOwnerSection, properties: { - hasTags: true, - hasTerms: true, + defaultOwnerType: OwnershipType.TechnicalOwner, }, }, { - component: SidebarOwnerSection, + component: SidebarTagsSection, properties: { - defaultOwnerType: OwnershipType.TechnicalOwner, + hasTags: true, + hasTerms: true, }, }, { diff --git a/datahub-web-react/src/app/entity/mlPrimaryKey/MLPrimaryKeyEntity.tsx b/datahub-web-react/src/app/entity/mlPrimaryKey/MLPrimaryKeyEntity.tsx index 5f0955164fe3e..c6b4bba46f331 100644 --- a/datahub-web-react/src/app/entity/mlPrimaryKey/MLPrimaryKeyEntity.tsx +++ b/datahub-web-react/src/app/entity/mlPrimaryKey/MLPrimaryKeyEntity.tsx @@ -89,16 +89,16 @@ export class MLPrimaryKeyEntity implements Entity { component: SidebarAboutSection, }, { - component: SidebarTagsSection, + component: SidebarOwnerSection, properties: { - hasTags: true, - hasTerms: true, + defaultOwnerType: OwnershipType.TechnicalOwner, }, }, { - component: SidebarOwnerSection, + component: SidebarTagsSection, properties: { - defaultOwnerType: OwnershipType.TechnicalOwner, + hasTags: true, + hasTerms: true, }, }, { diff --git a/datahub-web-react/src/app/entity/shared/components/styled/DemoButton.tsx b/datahub-web-react/src/app/entity/shared/components/styled/DemoButton.tsx new file mode 100644 index 0000000000000..1ed182fa01975 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/components/styled/DemoButton.tsx @@ -0,0 +1,22 @@ +import { Button } from 'antd'; +import React from 'react'; +import styled from 'styled-components'; + +const StyledButton = styled(Button)` + padding: 8px; + font-size: 14px; + margin-left: 18px; +`; + +export default function DemoButton() { + return ( + + Schedule a Demo + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/components/styled/ExpandedActorGroup.tsx b/datahub-web-react/src/app/entity/shared/components/styled/ExpandedActorGroup.tsx index 887e0f2421cbd..c5f96cda3c64d 100644 --- a/datahub-web-react/src/app/entity/shared/components/styled/ExpandedActorGroup.tsx +++ b/datahub-web-react/src/app/entity/shared/components/styled/ExpandedActorGroup.tsx @@ -4,7 +4,9 @@ import styled from 'styled-components'; import { CorpGroup, CorpUser } from '../../../../../types.generated'; import { ExpandedActor } from './ExpandedActor'; -const PopoverActors = styled.div``; +const PopoverActors = styled.div` + max-width: 600px; +`; const ActorsContainer = styled.div` display: flex; diff --git a/datahub-web-react/src/app/entity/shared/constants.ts b/datahub-web-react/src/app/entity/shared/constants.ts index ba399dd287803..bc40ba871e7d1 100644 --- a/datahub-web-react/src/app/entity/shared/constants.ts +++ b/datahub-web-react/src/app/entity/shared/constants.ts @@ -93,3 +93,10 @@ export const GLOSSARY_ENTITY_TYPES = [EntityType.GlossaryTerm, EntityType.Glossa export const DEFAULT_SYSTEM_ACTOR_URNS = ['urn:li:corpuser:__datahub_system', 'urn:li:corpuser:unknown']; export const VIEW_ENTITY_PAGE = 'VIEW_ENTITY_PAGE'; + +// only values for Domain Entity for custom configurable default tab +export enum EntityProfileTab { + DOMAIN_ENTITIES_TAB = 'DOMAIN_ENTITIES_TAB', + DOCUMENTATION_TAB = 'DOCUMENTATION_TAB', + DATA_PRODUCTS_TAB = 'DATA_PRODUCTS_TAB', +} 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 f6e839d727c02..8a559013c892c 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 @@ -8,6 +8,7 @@ import { Message } from '../../../../shared/Message'; import { getEntityPath, getOnboardingStepIdsForEntityType, + sortEntityProfileTabs, useRoutedTab, useUpdateGlossaryEntityDataOnChange, } from './utils'; @@ -43,6 +44,7 @@ import { LINEAGE_GRAPH_INTRO_ID, LINEAGE_GRAPH_TIME_FILTER_ID, } from '../../../../onboarding/config/LineageGraphOnboardingConfig'; +import { useAppConfig } from '../../../../useAppConfig'; type Props = { urn: string; @@ -168,8 +170,10 @@ export const EntityProfile = ({ const isHideSiblingMode = useIsSeparateSiblingsMode(); const entityRegistry = useEntityRegistry(); const history = useHistory(); + const appConfig = useAppConfig(); const isCompact = React.useContext(CompactContext); const tabsWithDefaults = tabs.map((tab) => ({ ...tab, display: { ...defaultTabDisplayConfig, ...tab.display } })); + const sortedTabs = sortEntityProfileTabs(appConfig.config, entityType, tabsWithDefaults); const sideBarSectionsWithDefaults = sidebarSections.map((sidebarSection) => ({ ...sidebarSection, display: { ...defaultSidebarSection, ...sidebarSection.display }, @@ -235,7 +239,7 @@ export const EntityProfile = ({ }, })) || []; - const visibleTabs = [...tabsWithDefaults, ...autoRenderTabs].filter((tab) => + const visibleTabs = [...sortedTabs, ...autoRenderTabs].filter((tab) => tab.display?.visible(entityData, dataPossiblyCombinedWithSiblings), ); diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/utils.ts b/datahub-web-react/src/app/entity/shared/containers/profile/utils.ts index 86fc4dac5aa4d..fabf0b2c51e95 100644 --- a/datahub-web-react/src/app/entity/shared/containers/profile/utils.ts +++ b/datahub-web-react/src/app/entity/shared/containers/profile/utils.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { useLocation } from 'react-router'; import queryString from 'query-string'; import { isEqual } from 'lodash'; -import { EntityType } from '../../../../../types.generated'; +import { AppConfig, EntityType } from '../../../../../types.generated'; import useIsLineageMode from '../../../../lineage/utils/useIsLineageMode'; import { useEntityRegistry } from '../../../../useEntityRegistry'; import EntityRegistry from '../../../EntityRegistry'; @@ -23,6 +23,13 @@ import { useGlossaryEntityData } from '../../GlossaryEntityContext'; import usePrevious from '../../../../shared/usePrevious'; import { GLOSSARY_ENTITY_TYPES } from '../../constants'; +/** + * The structure of our path will be + * + * /// + */ +const ENTITY_TAB_NAME_REGEX_PATTERN = '^/[^/]+/[^/]+/([^/]+).*'; + export function getDataForEntityType({ data: entityData, getOverrideProperties, @@ -103,18 +110,28 @@ export function useEntityPath(entityType: EntityType, urn: string, tabName?: str export function useRoutedTab(tabs: EntityTab[]): EntityTab | undefined { const { pathname } = useLocation(); const trimmedPathName = pathname.endsWith('/') ? pathname.slice(0, pathname.length - 1) : pathname; - const splitPathName = trimmedPathName.split('/'); - const lastTokenInPath = splitPathName[splitPathName.length - 1]; - const routedTab = tabs.find((tab) => tab.name === lastTokenInPath); - return routedTab; + // Match against the regex + const match = trimmedPathName.match(ENTITY_TAB_NAME_REGEX_PATTERN); + if (match && match[1]) { + const selectedTabPath = match[1]; + const routedTab = tabs.find((tab) => tab.name === selectedTabPath); + return routedTab; + } + // No match found! + return undefined; } export function useIsOnTab(tabName: string): boolean { const { pathname } = useLocation(); const trimmedPathName = pathname.endsWith('/') ? pathname.slice(0, pathname.length - 1) : pathname; - const splitPathName = trimmedPathName.split('/'); - const lastTokenInPath = splitPathName[splitPathName.length - 1]; - return lastTokenInPath === tabName; + // Match against the regex + const match = trimmedPathName.match(ENTITY_TAB_NAME_REGEX_PATTERN); + if (match && match[1]) { + const selectedTabPath = match[1]; + return selectedTabPath === tabName; + } + // No match found! + return false; } export function formatDateString(time: number) { @@ -185,3 +202,22 @@ export function getOnboardingStepIdsForEntityType(entityType: EntityType): strin return []; } } + +function sortTabsWithDefaultTabId(tabs: EntityTab[], defaultTabId: string) { + return tabs.sort((tabA, tabB) => { + if (tabA.id === defaultTabId) return -1; + if (tabB.id === defaultTabId) return 1; + return 0; + }); +} + +export function sortEntityProfileTabs(appConfig: AppConfig, entityType: EntityType, tabs: EntityTab[]) { + const sortedTabs = [...tabs]; + + if (entityType === EntityType.Domain && appConfig.visualConfig.entityProfiles?.domain?.defaultTab) { + const defaultTabId = appConfig.visualConfig.entityProfiles?.domain.defaultTab; + sortTabsWithDefaultTabId(sortedTabs, defaultTabId); + } + + return sortedTabs; +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Stats/historical/HistoricalStats.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Stats/historical/HistoricalStats.tsx index 118c984e41b65..8e5e8fde2898d 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Stats/historical/HistoricalStats.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Stats/historical/HistoricalStats.tsx @@ -10,6 +10,7 @@ import { Message } from '../../../../../../shared/Message'; import { LookbackWindow } from '../lookbackWindows'; import { ANTD_GRAY } from '../../../../constants'; import PrefixedSelect from './shared/PrefixedSelect'; +import { formatBytes } from '../../../../../../shared/formatNumber'; // TODO: Reuse stat sections. const StatSection = styled.div` @@ -167,6 +168,7 @@ export default function HistoricalStats({ urn, lookbackWindow }: Props) { */ const rowCountChartValues = extractChartValuesFromTableProfiles(profiles, 'rowCount'); const columnCountChartValues = extractChartValuesFromTableProfiles(profiles, 'columnCount'); + const sizeChartValues = extractChartValuesFromTableProfiles(profiles, 'sizeInBytes'); /** * Compute Column Stat chart data. @@ -192,6 +194,24 @@ export default function HistoricalStats({ urn, lookbackWindow }: Props) { 'uniqueProportion', ); + const bytesFormatter = (num: number) => { + const formattedBytes = formatBytes(num); + return `${formattedBytes.number} ${formattedBytes.unit}`; + }; + + const placeholderChart = ( + + ); + const placeholderVerticalDivider = ( + + ); + return ( <> {profilesLoading && } @@ -216,6 +236,18 @@ export default function HistoricalStats({ urn, lookbackWindow }: Props) { values={columnCountChartValues} /> + + + + {placeholderVerticalDivider} + {placeholderChart} + diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Stats/historical/charts/ProfilingRunsChart.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Stats/historical/charts/ProfilingRunsChart.tsx index 9b5176f96b697..11ec85d65da42 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Stats/historical/charts/ProfilingRunsChart.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Stats/historical/charts/ProfilingRunsChart.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import { DatasetProfile } from '../../../../../../../../types.generated'; import ColumnStats from '../../snapshot/ColumnStats'; import TableStats from '../../snapshot/TableStats'; +import { formatBytes, formatNumberWithoutAbbreviation } from '../../../../../../../shared/formatNumber'; export const ChartTable = styled(Table)` margin-top: 16px; @@ -13,6 +14,12 @@ export type Props = { profiles: Array; }; +const bytesFormatter = (bytes: number) => { + const formattedBytes = formatBytes(bytes); + const fullBytes = formatNumberWithoutAbbreviation(bytes); + return `${formattedBytes.number} ${formattedBytes.unit} (${fullBytes} bytes)`; +}; + export default function ProfilingRunsChart({ profiles }: Props) { const [showModal, setShowModal] = useState(false); const [selectedProfileIndex, setSelectedProfileIndex] = useState(-1); @@ -33,6 +40,7 @@ export default function ProfilingRunsChart({ profiles }: Props) { timestamp: `${profileDate.toLocaleDateString()} at ${profileDate.toLocaleTimeString()}`, rowCount: profile.rowCount?.toString() || 'unknown', columnCount: profile.columnCount?.toString() || 'unknown', + sizeInBytes: profile.sizeInBytes ? bytesFormatter(profile.sizeInBytes) : 'unknown', }; }); @@ -59,6 +67,11 @@ export default function ProfilingRunsChart({ profiles }: Props) { key: 'Column Count', dataIndex: 'columnCount', }, + { + title: 'Size', + key: 'Size', + dataIndex: 'sizeInBytes', + }, ]; const selectedProfile = (selectedProfileIndex >= 0 && profiles[selectedProfileIndex]) || undefined; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Stats/historical/charts/StatChart.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Stats/historical/charts/StatChart.tsx index a0531f33d1b3a..db5b1a59759b1 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Stats/historical/charts/StatChart.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Stats/historical/charts/StatChart.tsx @@ -14,8 +14,9 @@ const ChartTitle = styled(Typography.Text)` } `; -const ChartCard = styled(Card)` +const ChartCard = styled(Card)<{ visible: boolean }>` box-shadow: ${(props) => props.theme.styles['box-shadow']}; + visibility: ${(props) => (props.visible ? 'visible' : 'hidden')}; ; `; type Point = { @@ -23,11 +24,17 @@ type Point = { value: number; }; +type AxisConfig = { + formatter: (tick: number) => string; +}; + export type Props = { title: string; values: Array; tickInterval: DateInterval; dateRange: DateRange; + yAxis?: AxisConfig; + visible?: boolean; }; /** @@ -41,7 +48,7 @@ const DEFAULT_AXIS_WIDTH = 2; /** * Time Series Chart with a single line. */ -export default function StatChart({ title, values, tickInterval: interval, dateRange }: Props) { +export default function StatChart({ title, values, tickInterval: interval, dateRange, yAxis, visible = true }: Props) { const timeSeriesData = useMemo( () => values @@ -56,22 +63,10 @@ export default function StatChart({ title, values, tickInterval: interval, dateR [values], ); - const chartData = { - title, - lines: [ - { - name: 'line_1', - data: timeSeriesData, - }, - ], - interval, - dateRange, - }; - return ( - + - {chartData.title} + {title} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx new file mode 100644 index 0000000000000..68660164ee877 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import { useGetDatasetAssertionsQuery } from '../../../../../../graphql/dataset.generated'; +import { Assertion, AssertionResultType } from '../../../../../../types.generated'; +import { useEntityData } from '../../../EntityContext'; +import { DatasetAssertionsList } from './DatasetAssertionsList'; +import { DatasetAssertionsSummary } from './DatasetAssertionsSummary'; +import { sortAssertions } from './assertionUtils'; +import { combineEntityDataWithSiblings, useIsSeparateSiblingsMode } from '../../../siblingUtils'; + +/** + * Returns a status summary for the assertions associated with a Dataset. + */ +const getAssertionsStatusSummary = (assertions: Array) => { + const summary = { + failedRuns: 0, + succeededRuns: 0, + totalRuns: 0, + totalAssertions: assertions.length, + }; + assertions.forEach((assertion) => { + if ((assertion.runEvents?.runEvents?.length || 0) > 0) { + const mostRecentRun = assertion.runEvents?.runEvents?.[0]; + const resultType = mostRecentRun?.result?.type; + if (AssertionResultType.Success === resultType) { + summary.succeededRuns++; + } + if (AssertionResultType.Failure === resultType) { + summary.failedRuns++; + } + summary.totalRuns++; // only count assertions for which there is one completed run event! + } + }); + return summary; +}; + +/** + * Component used for rendering the Validations Tab on the Dataset Page. + */ +export const Assertions = () => { + const { urn, entityData } = useEntityData(); + const { data, refetch } = useGetDatasetAssertionsQuery({ variables: { urn }, fetchPolicy: 'cache-first' }); + const isHideSiblingMode = useIsSeparateSiblingsMode(); + + const combinedData = isHideSiblingMode ? data : combineEntityDataWithSiblings(data); + const [removedUrns, setRemovedUrns] = useState([]); + + const assertions = + (combinedData && combinedData.dataset?.assertions?.assertions?.map((assertion) => assertion as Assertion)) || + []; + const filteredAssertions = assertions.filter((assertion) => !removedUrns.includes(assertion.urn)); + + // Pre-sort the list of assertions based on which has been most recently executed. + assertions.sort(sortAssertions); + + return ( + <> + + {entityData && ( + { + // Hack to deal with eventual consistency. + setRemovedUrns([...removedUrns, assertionUrn]); + setTimeout(() => refetch(), 3000); + }} + /> + )} + + ); +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/ValidationsTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/ValidationsTab.tsx index 3276ffd4a602d..b4f77196edbb1 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/ValidationsTab.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/ValidationsTab.tsx @@ -1,112 +1,99 @@ -import { FileDoneOutlined, FileProtectOutlined } from '@ant-design/icons'; +import React, { useEffect } from 'react'; import { Button } from 'antd'; -import React, { useEffect, useState } from 'react'; -import { useGetDatasetAssertionsQuery } from '../../../../../../graphql/dataset.generated'; -import { Assertion, AssertionResultType } from '../../../../../../types.generated'; -import TabToolbar from '../../../components/styled/TabToolbar'; +import { useHistory, useLocation } from 'react-router'; +import styled from 'styled-components'; +import { FileDoneOutlined, FileProtectOutlined } from '@ant-design/icons'; import { useEntityData } from '../../../EntityContext'; -import { DatasetAssertionsList } from './DatasetAssertionsList'; -import { DatasetAssertionsSummary } from './DatasetAssertionsSummary'; -import { sortAssertions } from './assertionUtils'; import { TestResults } from './TestResults'; -import { combineEntityDataWithSiblings, useIsSeparateSiblingsMode } from '../../../siblingUtils'; +import { Assertions } from './Assertions'; +import TabToolbar from '../../../components/styled/TabToolbar'; +import { useGetValidationsTab } from './useGetValidationsTab'; +import { ANTD_GRAY } from '../../../constants'; -/** - * Returns a status summary for the assertions associated with a Dataset. - */ -const getAssertionsStatusSummary = (assertions: Array) => { - const summary = { - failedRuns: 0, - succeededRuns: 0, - totalRuns: 0, - totalAssertions: assertions.length, - }; - assertions.forEach((assertion) => { - if ((assertion.runEvents?.runEvents?.length || 0) > 0) { - const mostRecentRun = assertion.runEvents?.runEvents?.[0]; - const resultType = mostRecentRun?.result?.type; - if (AssertionResultType.Success === resultType) { - summary.succeededRuns++; - } - if (AssertionResultType.Failure === resultType) { - summary.failedRuns++; - } - summary.totalRuns++; // only count assertions for which there is one completed run event! - } - }); - return summary; -}; +const TabTitle = styled.span` + margin-left: 4px; +`; + +const TabButton = styled(Button)<{ selected: boolean }>` + background-color: ${(props) => (props.selected && ANTD_GRAY[3]) || 'none'}; + margin-left: 4px; +`; -enum ViewType { - ASSERTIONS, - TESTS, +enum TabPaths { + ASSERTIONS = 'Assertions', + TESTS = 'Tests', } +const DEFAULT_TAB = TabPaths.ASSERTIONS; + /** - * Component used for rendering the Validations Tab on the Dataset Page. + * Component used for rendering the Entity Validations Tab. */ export const ValidationsTab = () => { - const { urn, entityData } = useEntityData(); - const { data, refetch } = useGetDatasetAssertionsQuery({ variables: { urn }, fetchPolicy: 'cache-first' }); - const isHideSiblingMode = useIsSeparateSiblingsMode(); - - const combinedData = isHideSiblingMode ? data : combineEntityDataWithSiblings(data); - const [removedUrns, setRemovedUrns] = useState([]); - /** - * Determines which view should be visible: assertions or tests. - */ - const [view, setView] = useState(ViewType.ASSERTIONS); - - const assertions = - (combinedData && combinedData.dataset?.assertions?.assertions?.map((assertion) => assertion as Assertion)) || - []; - const filteredAssertions = assertions.filter((assertion) => !removedUrns.includes(assertion.urn)); - const numAssertions = filteredAssertions.length; + const { entityData } = useEntityData(); + const history = useHistory(); + const { pathname } = useLocation(); + const totalAssertions = (entityData as any)?.assertions?.total; const passingTests = (entityData as any)?.testResults?.passing || []; const maybeFailingTests = (entityData as any)?.testResults?.failing || []; const totalTests = maybeFailingTests.length + passingTests.length; + const { selectedTab, basePath } = useGetValidationsTab(pathname, Object.values(TabPaths)); + + // If no tab was selected, select a default tab. useEffect(() => { - if (totalTests > 0 && numAssertions === 0) { - setView(ViewType.TESTS); - } else { - setView(ViewType.ASSERTIONS); + if (!selectedTab) { + // Route to the default tab. + history.replace(`${basePath}/${DEFAULT_TAB}`); } - }, [totalTests, numAssertions]); + }, [selectedTab, basePath, history]); - // Pre-sort the list of assertions based on which has been most recently executed. - assertions.sort(sortAssertions); + /** + * The top-level Toolbar tabs to display. + */ + const tabs = [ + { + title: ( + <> + + Assertions ({totalAssertions}) + + ), + path: TabPaths.ASSERTIONS, + disabled: totalAssertions === 0, + content: , + }, + { + title: ( + <> + + Tests ({totalTests}) + + ), + path: TabPaths.TESTS, + disabled: totalTests === 0, + content: , + }, + ]; return ( <>
- - + {tabs.map((tab) => ( + history.replace(`${basePath}/${tab.path}`)} + > + {tab.title} + + ))}
- {(view === ViewType.ASSERTIONS && ( - <> - - {entityData && ( - { - // Hack to deal with eventual consistency. - setRemovedUrns([...removedUrns, assertionUrn]); - setTimeout(() => refetch(), 3000); - }} - /> - )} - - )) || } + {tabs.filter((tab) => tab.path === selectedTab).map((tab) => tab.content)} ); }; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/__tests__/useGetValidationsTab.test.ts b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/__tests__/useGetValidationsTab.test.ts new file mode 100644 index 0000000000000..52689a225eae1 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/__tests__/useGetValidationsTab.test.ts @@ -0,0 +1,38 @@ +import { useGetValidationsTab } from '../useGetValidationsTab'; + +describe('useGetValidationsTab', () => { + it('should correctly extract valid tab', () => { + const pathname = '/dataset/urn:li:abc/Validation/Assertions'; + const tabNames = ['Assertions']; + const res = useGetValidationsTab(pathname, tabNames); + expect(res.selectedTab).toEqual('Assertions'); + expect(res.basePath).toEqual('/dataset/urn:li:abc/Validation'); + }); + it('should extract undefined for invalid tab', () => { + const pathname = '/dataset/urn:li:abc/Validation/Assertions'; + const tabNames = ['Tests']; + const res = useGetValidationsTab(pathname, tabNames); + expect(res.selectedTab).toBeUndefined(); + expect(res.basePath).toEqual('/dataset/urn:li:abc/Validation'); + }); + it('should extract undefined for missing tab', () => { + const pathname = '/dataset/urn:li:abc/Validation'; + const tabNames = ['Tests']; + const res = useGetValidationsTab(pathname, tabNames); + expect(res.selectedTab).toBeUndefined(); + expect(res.basePath).toEqual('/dataset/urn:li:abc/Validation'); + }); + it('should handle trailing slashes', () => { + let pathname = '/dataset/urn:li:abc/Validation/Assertions/'; + let tabNames = ['Assertions']; + let res = useGetValidationsTab(pathname, tabNames); + expect(res.selectedTab).toEqual('Assertions'); + expect(res.basePath).toEqual('/dataset/urn:li:abc/Validation'); + + pathname = '/dataset/urn:li:abc/Validation/'; + tabNames = ['Assertions']; + res = useGetValidationsTab(pathname, tabNames); + expect(res.selectedTab).toBeUndefined(); + expect(res.basePath).toEqual('/dataset/urn:li:abc/Validation'); + }); +}); diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/useGetValidationsTab.ts b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/useGetValidationsTab.ts new file mode 100644 index 0000000000000..164e8e489e346 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/useGetValidationsTab.ts @@ -0,0 +1,35 @@ +/** + * The structure of our path will be + * + * ///Validation/ + */ +const VALIDATION_TAB_NAME_REGEX_PATTERN = '^/[^/]+/[^/]+/[^/]+/([^/]+).*'; + +export type SelectedTab = { + basePath: string; + selectedTab: string | undefined; +}; + +/** + * Returns information about the currently selected Validations Tab path. + * + * This is determined by parsing the current URL path and attempting to match against a set of + * valid path names. If a matching tab cannot be found, then the selected tab will be returned as undefined. + */ +export const useGetValidationsTab = (pathname: string, tabNames: string[]): SelectedTab => { + const trimmedPathName = pathname.endsWith('/') ? pathname.slice(0, pathname.length - 1) : pathname; + const match = trimmedPathName.match(VALIDATION_TAB_NAME_REGEX_PATTERN); + if (match && match[1]) { + const selectedTabPath = match[1]; + const routedTab = tabNames.find((tab) => tab === selectedTabPath); + return { + basePath: trimmedPathName.substring(0, trimmedPathName.lastIndexOf('/')), + selectedTab: routedTab, + }; + } + // No match found! + return { + basePath: trimmedPathName, + selectedTab: undefined, + }; +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/LinkList.tsx b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/LinkList.tsx index eddfbd34aec5d..1aef497ced57b 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/LinkList.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/LinkList.tsx @@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'; import styled from 'styled-components/macro'; import { message, Button, List, Typography } from 'antd'; import { LinkOutlined, DeleteOutlined } from '@ant-design/icons'; -import { EntityType } from '../../../../../../types.generated'; +import { EntityType, InstitutionalMemoryMetadata } from '../../../../../../types.generated'; import { useEntityData } from '../../../EntityContext'; import { useEntityRegistry } from '../../../../../useEntityRegistry'; import { ANTD_GRAY } from '../../../constants'; @@ -33,15 +33,15 @@ type LinkListProps = { }; export const LinkList = ({ refetch }: LinkListProps) => { - const { urn, entityData } = useEntityData(); + const { entityData } = useEntityData(); const entityRegistry = useEntityRegistry(); const [removeLinkMutation] = useRemoveLinkMutation(); const links = entityData?.institutionalMemory?.elements || []; - const handleDeleteLink = async (linkUrl: string) => { + const handleDeleteLink = async (metadata: InstitutionalMemoryMetadata) => { try { await removeLinkMutation({ - variables: { input: { linkUrl, resourceUrn: urn } }, + variables: { input: { linkUrl: metadata.url, resourceUrn: metadata.associatedUrn } }, }); message.success({ content: 'Link Removed', duration: 2 }); } catch (e: unknown) { @@ -62,7 +62,7 @@ export const LinkList = ({ refetch }: LinkListProps) => { renderItem={(link) => ( handleDeleteLink(link.url)} type="text" shape="circle" danger> + } diff --git a/datahub-web-react/src/app/entity/shared/types.ts b/datahub-web-react/src/app/entity/shared/types.ts index d1fbfe41fc66d..e36f5050a24b7 100644 --- a/datahub-web-react/src/app/entity/shared/types.ts +++ b/datahub-web-react/src/app/entity/shared/types.ts @@ -36,6 +36,7 @@ import { Embed, FabricType, BrowsePathV2, + DataJobInputOutput, } from '../../../types.generated'; import { FetchedEntity } from '../../lineage/types'; @@ -47,6 +48,7 @@ export type EntityTab = { enabled: (GenericEntityProperties, T) => boolean; // Whether the tab is enabled on the UI. Defaults to true. }; properties?: any; + id?: string; }; export type EntitySidebarSection = { @@ -109,6 +111,7 @@ export type GenericEntityProperties = { exists?: boolean; origin?: Maybe; browsePathV2?: Maybe; + inputOutput?: Maybe; }; export type GenericEntityUpdate = { diff --git a/datahub-web-react/src/app/entity/shared/utils.ts b/datahub-web-react/src/app/entity/shared/utils.ts index 2180e36d410cc..7ec604785d1ff 100644 --- a/datahub-web-react/src/app/entity/shared/utils.ts +++ b/datahub-web-react/src/app/entity/shared/utils.ts @@ -153,7 +153,9 @@ export function getFineGrainedLineageWithSiblings( entityData: GenericEntityProperties | null, getGenericEntityProperties: (type: EntityType, data: Entity) => GenericEntityProperties | null, ) { - const fineGrainedLineages = [...(entityData?.fineGrainedLineages || [])]; + const fineGrainedLineages = [ + ...(entityData?.fineGrainedLineages || entityData?.inputOutput?.fineGrainedLineages || []), + ]; entityData?.siblings?.siblings?.forEach((sibling) => { if (sibling) { const genericSiblingProps = getGenericEntityProperties(sibling.type, sibling); diff --git a/datahub-web-react/src/app/home/AcrylDemoBanner.tsx b/datahub-web-react/src/app/home/AcrylDemoBanner.tsx new file mode 100644 index 0000000000000..0a6316a71db16 --- /dev/null +++ b/datahub-web-react/src/app/home/AcrylDemoBanner.tsx @@ -0,0 +1,69 @@ +import Link from 'antd/lib/typography/Link'; +import React from 'react'; +import styled from 'styled-components'; +import AcrylLogo from '../../images/acryl-light-mark.svg'; + +const BannerWrapper = styled.div` + padding: 12px; + display: flex; + align-items: center; + justify-content: center; + color: #262626; + background-color: #e6f4ff; + width: 100%; + margin-bottom: 24px; +`; + +const Logo = styled.img` + margin-right: 12px; + height: 40px; + width: 40px; +`; + +const TextWrapper = styled.div` + font-size: 14px; +`; + +const Title = styled.div` + font-weight: 700; +`; + +const StyledLink = styled(Link)` + color: #1890ff; + font-weight: 700; +`; + +const TextContent = styled.div` + max-width: 1025px; +`; + +export default function AcrylDemoBanner() { + return ( + + + + Schedule a Demo of Managed DataHub + + DataHub is already the industry's #1 Open Source Data Catalog.{' '} + + Schedule a demo + {' '} + of Acryl DataHub to see the advanced features that take it to the next level or purchase Acryl Cloud + on{' '} + + AWS Marketplace + + ! + + + + ); +} diff --git a/datahub-web-react/src/app/home/HomePageHeader.tsx b/datahub-web-react/src/app/home/HomePageHeader.tsx index 1d61eff66ed25..02c847bb040a5 100644 --- a/datahub-web-react/src/app/home/HomePageHeader.tsx +++ b/datahub-web-react/src/app/home/HomePageHeader.tsx @@ -16,12 +16,14 @@ import { EntityType, FacetFilterInput } from '../../types.generated'; import analytics, { EventType } from '../analytics'; import { HeaderLinks } from '../shared/admin/HeaderLinks'; import { ANTD_GRAY } from '../entity/shared/constants'; -import { useAppConfig } from '../useAppConfig'; +import { useAppConfig, useIsShowAcrylInfoEnabled } from '../useAppConfig'; import { DEFAULT_APP_CONFIG } from '../../appConfigContext'; import { HOME_PAGE_SEARCH_BAR_ID } from '../onboarding/config/HomePageOnboardingConfig'; import { useQuickFiltersContext } from '../../providers/QuickFiltersContext'; import { getAutoCompleteInputFromQuickFilter } from '../search/utils/filterUtils'; import { useUserContext } from '../context/useUserContext'; +import AcrylDemoBanner from './AcrylDemoBanner'; +import DemoButton from '../entity/shared/components/styled/DemoButton'; const Background = styled.div` width: 100%; @@ -147,6 +149,7 @@ export const HomePageHeader = () => { const appConfig = useAppConfig(); const [newSuggestionData, setNewSuggestionData] = useState(); const { selectedQuickFilter } = useQuickFiltersContext(); + const showAcrylInfo = useIsShowAcrylInfoEnabled(); const { user } = userContext; const viewUrn = userContext.localState?.selectedViewUrn; @@ -243,9 +246,11 @@ export const HomePageHeader = () => { pictureLink={user?.editableProperties?.pictureLink || ''} name={(user && entityRegistry.getDisplayName(EntityType.CorpUser, user)) || undefined} /> + {showAcrylInfo && } + {showAcrylInfo && } { if (existingRecipeYaml) { + setStagedRecipeName(state.name); setStagedRecipeYml(existingRecipeYaml); } - }, [existingRecipeYaml]); + }, [existingRecipeYaml, state.name]); const [stepComplete, setStepComplete] = useState(false); @@ -97,6 +99,7 @@ export const DefineRecipeStep = ({ state, updateState, goTo, prev, ingestionSour if (type && CONNECTORS_WITH_FORM.has(type)) { return ( ; siblingPlatforms?: Maybe; - fineGrainedLineages?: [FineGrainedLineage]; + fineGrainedLineages?: FineGrainedLineage[]; siblings?: Maybe; schemaMetadata?: SchemaMetadata; inputFields?: InputFields; diff --git a/datahub-web-react/src/app/lineage/utils/__tests__/columnLineageUtils.test.tsx b/datahub-web-react/src/app/lineage/utils/__tests__/columnLineageUtils.test.tsx index 0b3d6886ec1c5..cd0a5f1385858 100644 --- a/datahub-web-react/src/app/lineage/utils/__tests__/columnLineageUtils.test.tsx +++ b/datahub-web-react/src/app/lineage/utils/__tests__/columnLineageUtils.test.tsx @@ -2,8 +2,12 @@ import { decodeSchemaField, encodeSchemaField, getFieldPathFromSchemaFieldUrn, + getPopulatedColumnsByUrn, getSourceUrnFromSchemaFieldUrn, } from '../columnLineageUtils'; +import { dataJob1, dataset1, dataset2 } from '../../../../Mocks'; +import { FetchedEntity } from '../../types'; +import { FineGrainedLineage, SchemaFieldDataType } from '../../../../types.generated'; describe('getSourceUrnFromSchemaFieldUrn', () => { it('should get the source urn for a chart schemaField', () => { @@ -82,3 +86,43 @@ describe('encodeSchemaField', () => { expect(decodedSchemaField).toBe(schemaField); }); }); + +describe('getPopulatedColumnsByUrn', () => { + it('should update columns by urn with data job fine grained data so that the data job appears to have the upstream columns', () => { + const dataJobWithCLL = { + ...dataJob1, + name: '', + fineGrainedLineages: [ + { + upstreams: [{ urn: dataset1.urn, path: 'test1' }], + downstreams: [{ urn: dataset2.urn, path: 'test2' }], + }, + { + upstreams: [{ urn: dataset1.urn, path: 'test3' }], + downstreams: [{ urn: dataset2.urn, path: 'test4' }], + }, + ] as FineGrainedLineage[], + }; + const fetchedEntities = { + [dataJobWithCLL.urn]: dataJobWithCLL as FetchedEntity, + }; + const columnsByUrn = getPopulatedColumnsByUrn({}, fetchedEntities); + + expect(columnsByUrn).toMatchObject({ + [dataJobWithCLL.urn]: [ + { + fieldPath: 'test1', + nullable: false, + recursive: false, + type: SchemaFieldDataType.String, + }, + { + fieldPath: 'test3', + nullable: false, + recursive: false, + type: SchemaFieldDataType.String, + }, + ], + }); + }); +}); diff --git a/datahub-web-react/src/app/lineage/utils/__tests__/extendAsyncEntities.test.ts b/datahub-web-react/src/app/lineage/utils/__tests__/extendAsyncEntities.test.ts new file mode 100644 index 0000000000000..ad28bccbbd85a --- /dev/null +++ b/datahub-web-react/src/app/lineage/utils/__tests__/extendAsyncEntities.test.ts @@ -0,0 +1,39 @@ +import { dataJob1, dataset1, dataset2 } from '../../../../Mocks'; +import { FetchedEntity } from '../../types'; +import { FineGrainedLineage } from '../../../../types.generated'; +import { extendColumnLineage } from '../extendAsyncEntities'; + +describe('extendColumnLineage', () => { + it('should update fineGrainedMap to draw lines from downstream and upstream datasets with a datajob in between', () => { + const dataJobWithCLL = { + ...dataJob1, + name: '', + fineGrainedLineages: [ + { + upstreams: [{ urn: dataset1.urn, path: 'test1' }], + downstreams: [{ urn: dataset2.urn, path: 'test2' }], + }, + { + upstreams: [{ urn: dataset1.urn, path: 'test3' }], + downstreams: [{ urn: dataset2.urn, path: 'test4' }], + }, + ] as FineGrainedLineage[], + }; + const fetchedEntities = { + [dataJobWithCLL.urn]: dataJobWithCLL as FetchedEntity, + }; + const fineGrainedMap = { forward: {}, reverse: {} }; + extendColumnLineage(dataJobWithCLL, fineGrainedMap, {}, fetchedEntities); + + expect(fineGrainedMap).toMatchObject({ + forward: { + [dataJob1.urn]: { test1: { [dataset2.urn]: ['test2'] }, test3: { [dataset2.urn]: ['test4'] } }, + [dataset1.urn]: { test1: { [dataJob1.urn]: ['test1'] }, test3: { [dataJob1.urn]: ['test3'] } }, + }, + reverse: { + [dataJob1.urn]: { test1: { [dataset1.urn]: ['test1'] }, test3: { [dataset1.urn]: ['test3'] } }, + [dataset2.urn]: { test4: { [dataJob1.urn]: ['test3'] }, test2: { [dataJob1.urn]: ['test1'] } }, + }, + }); + }); +}); diff --git a/datahub-web-react/src/app/lineage/utils/columnLineageUtils.ts b/datahub-web-react/src/app/lineage/utils/columnLineageUtils.ts index 140f22cdea565..505b3d94531b7 100644 --- a/datahub-web-react/src/app/lineage/utils/columnLineageUtils.ts +++ b/datahub-web-react/src/app/lineage/utils/columnLineageUtils.ts @@ -1,5 +1,5 @@ import { ColumnEdge, FetchedEntity, NodeData } from '../types'; -import { InputFields, SchemaField } from '../../../types.generated'; +import { EntityType, InputFields, SchemaField, SchemaFieldDataType } from '../../../types.generated'; import { downgradeV2FieldPath } from '../../entity/dataset/profile/schema/utils/utils'; export function getHighlightedColumnsForNode(highlightedEdges: ColumnEdge[], fields: SchemaField[], nodeUrn: string) { @@ -63,10 +63,15 @@ export function convertInputFieldsToSchemaFields(inputFields?: InputFields) { return inputFields?.fields?.map((field) => field?.schemaField) as SchemaField[] | undefined; } -export function populateColumnsByUrn( +/* + * Populate a columnsByUrn map with a list of columns per entity in the order that they will appear. + * We need columnsByUrn in order to ensure that an entity does have a column that lineage data is + * pointing to and to know where to draw column arrows in and out of the entity. DataJobs won't show columns + * underneath them, but we need this populated for validating that this column "exists" on the entity. + */ +export function getPopulatedColumnsByUrn( columnsByUrn: Record, fetchedEntities: { [x: string]: FetchedEntity }, - setColumnsByUrn: (colsByUrn: Record) => void, ) { let populatedColumnsByUrn = { ...columnsByUrn }; Object.entries(fetchedEntities).forEach(([urn, fetchedEntity]) => { @@ -82,9 +87,35 @@ export function populateColumnsByUrn( convertInputFieldsToSchemaFields(fetchedEntity.inputFields) as SchemaField[], ), }; + } else if (fetchedEntity.type === EntityType.DataJob && fetchedEntity.fineGrainedLineages) { + // Add upstream fields from fineGrainedLineage onto DataJob to mimic upstream dataset fields. + // DataJobs will virtually "have" these fields so we can draw full column paths + // from upstream dataset fields to downstream dataset fields. + const fields: SchemaField[] = []; + fetchedEntity.fineGrainedLineages.forEach((fineGrainedLineage) => { + fineGrainedLineage.upstreams?.forEach((upstream) => { + if (!fields.some((field) => field.fieldPath === upstream.path)) { + fields.push({ + fieldPath: downgradeV2FieldPath(upstream.path) || '', + nullable: false, + recursive: false, + type: SchemaFieldDataType.String, + }); + } + }); + }); + populatedColumnsByUrn = { ...populatedColumnsByUrn, [urn]: fields }; } }); - setColumnsByUrn(populatedColumnsByUrn); + return populatedColumnsByUrn; +} + +export function populateColumnsByUrn( + columnsByUrn: Record, + fetchedEntities: { [x: string]: FetchedEntity }, + setColumnsByUrn: (colsByUrn: Record) => void, +) { + setColumnsByUrn(getPopulatedColumnsByUrn(columnsByUrn, fetchedEntities)); } export function haveDisplayedFieldsChanged(displayedFields: SchemaField[], previousDisplayedFields?: SchemaField[]) { @@ -127,6 +158,21 @@ export function encodeSchemaField(fieldPath: string) { export function getSourceUrnFromSchemaFieldUrn(schemaFieldUrn: string) { return schemaFieldUrn.replace('urn:li:schemaField:(', '').split(')')[0].concat(')'); } + export function getFieldPathFromSchemaFieldUrn(schemaFieldUrn: string) { return decodeSchemaField(schemaFieldUrn.replace('urn:li:schemaField:(', '').split(')')[1].replace(',', '')); } + +export function isSameColumn({ + sourceUrn, + targetUrn, + sourceField, + targetField, +}: { + sourceUrn: string; + targetUrn: string; + sourceField: string; + targetField: string; +}) { + return sourceUrn === targetUrn && sourceField === targetField; +} diff --git a/datahub-web-react/src/app/lineage/utils/extendAsyncEntities.ts b/datahub-web-react/src/app/lineage/utils/extendAsyncEntities.ts index 82fa489e8ef40..860b5715f34c9 100644 --- a/datahub-web-react/src/app/lineage/utils/extendAsyncEntities.ts +++ b/datahub-web-react/src/app/lineage/utils/extendAsyncEntities.ts @@ -1,10 +1,11 @@ -import { SchemaFieldRef } from '../../../types.generated'; +import { EntityType, SchemaFieldRef } from '../../../types.generated'; import EntityRegistry from '../../entity/EntityRegistry'; import { EntityAndType, FetchedEntities, FetchedEntity } from '../types'; import { decodeSchemaField, getFieldPathFromSchemaFieldUrn, getSourceUrnFromSchemaFieldUrn, + isSameColumn, } from './columnLineageUtils'; const breakFieldUrn = (ref: SchemaFieldRef) => { @@ -21,6 +22,17 @@ function updateFineGrainedMap( downstreamEntityUrn: string, downstreamField: string, ) { + // ignore self-referential CLL fields + if ( + isSameColumn({ + sourceUrn: upstreamEntityUrn, + targetUrn: downstreamEntityUrn, + sourceField: upstreamField, + targetField: downstreamField, + }) + ) { + return; + } const mapForUrn = fineGrainedMap.forward[upstreamEntityUrn] || {}; const mapForField = mapForUrn[upstreamField] || {}; const listForDownstream = [...(mapForField[downstreamEntityUrn] || [])]; @@ -42,7 +54,7 @@ function updateFineGrainedMap( mapForFieldReverse[upstreamEntityUrn] = listForDownstreamReverse; } -function extendColumnLineage( +export function extendColumnLineage( lineageVizConfig: FetchedEntity, fineGrainedMap: any, fineGrainedMapForSiblings: any, @@ -52,18 +64,42 @@ function extendColumnLineage( lineageVizConfig.fineGrainedLineages.forEach((fineGrainedLineage) => { fineGrainedLineage.upstreams?.forEach((upstream) => { const [upstreamEntityUrn, upstreamField] = breakFieldUrn(upstream); - fineGrainedLineage.downstreams?.forEach((downstream) => { - const downstreamField = breakFieldUrn(downstream)[1]; - // fineGrainedLineage always belongs on the downstream urn with upstreams pointing to another entity - // pass in the visualized node's urn and not the urn from the schema field as the downstream urn, - // as they will either be the same or if they are different, it belongs to a "hidden" sibling + + if (lineageVizConfig.type === EntityType.DataJob) { + // draw a line from upstream dataset field to datajob updateFineGrainedMap( fineGrainedMap, upstreamEntityUrn, upstreamField, lineageVizConfig.urn, - downstreamField, + upstreamField, ); + } + + fineGrainedLineage.downstreams?.forEach((downstream) => { + const [downstreamEntityUrn, downstreamField] = breakFieldUrn(downstream); + + if (lineageVizConfig.type === EntityType.DataJob) { + // draw line from datajob upstream field to downstream fields + updateFineGrainedMap( + fineGrainedMap, + lineageVizConfig.urn, + upstreamField, + downstreamEntityUrn, + downstreamField, + ); + } else { + // fineGrainedLineage always belongs on the downstream urn with upstreams pointing to another entity + // pass in the visualized node's urn and not the urn from the schema field as the downstream urn, + // as they will either be the same or if they are different, it belongs to a "hidden" sibling + updateFineGrainedMap( + fineGrainedMap, + upstreamEntityUrn, + upstreamField, + lineageVizConfig.urn, + downstreamField, + ); + } // upstreamEntityUrn could belong to a sibling we don't "render", so store its inputs to updateFineGrainedMap // and update the fine grained map later when we see the entity with these siblings diff --git a/datahub-web-react/src/app/lineage/utils/highlightColumnLineage.ts b/datahub-web-react/src/app/lineage/utils/highlightColumnLineage.ts index 7d7b0220ebfee..eaff8f958837b 100644 --- a/datahub-web-react/src/app/lineage/utils/highlightColumnLineage.ts +++ b/datahub-web-react/src/app/lineage/utils/highlightColumnLineage.ts @@ -1,3 +1,4 @@ +import { isEqual } from 'lodash'; import { ColumnEdge } from '../types'; function highlightDownstreamColumnLineage( @@ -11,8 +12,11 @@ function highlightDownstreamColumnLineage( Object.entries(forwardLineage).forEach((entry) => { const [targetUrn, fieldPaths] = entry; (fieldPaths as string[]).forEach((targetField) => { - edges.push({ sourceUrn, sourceField, targetUrn, targetField }); - highlightDownstreamColumnLineage(targetField, targetUrn, edges, fineGrainedMap); + const edge: ColumnEdge = { sourceUrn, sourceField, targetUrn, targetField }; + if (!edges.some((value) => isEqual(value, edge))) { + edges.push(edge); + highlightDownstreamColumnLineage(targetField, targetUrn, edges, fineGrainedMap); + } }); }); } @@ -29,8 +33,11 @@ function highlightUpstreamColumnLineage( Object.entries(reverseLineage).forEach((entry) => { const [sourceUrn, fieldPaths] = entry; (fieldPaths as string[]).forEach((sourceField) => { - edges.push({ targetUrn, targetField, sourceUrn, sourceField }); - highlightUpstreamColumnLineage(sourceField, sourceUrn, edges, fineGrainedMap); + const edge: ColumnEdge = { sourceUrn, sourceField, targetUrn, targetField }; + if (!edges.some((value) => isEqual(value, edge))) { + edges.push(edge); + highlightUpstreamColumnLineage(sourceField, sourceUrn, edges, fineGrainedMap); + } }); }); } diff --git a/datahub-web-react/src/app/lineage/utils/layoutTree.ts b/datahub-web-react/src/app/lineage/utils/layoutTree.ts index fa82d89f5066c..cc704007049c2 100644 --- a/datahub-web-react/src/app/lineage/utils/layoutTree.ts +++ b/datahub-web-react/src/app/lineage/utils/layoutTree.ts @@ -1,4 +1,4 @@ -import { SchemaField } from '../../../types.generated'; +import { EntityType, SchemaField } from '../../../types.generated'; import { COLUMN_HEIGHT, CURVE_PADDING, @@ -203,9 +203,12 @@ function drawColumnEdge({ visibleColumnsByUrn, }: DrawColumnEdgeProps) { const targetFieldIndex = targetFields.findIndex((candidate) => candidate.fieldPath === targetField) || 0; - const targetFieldY = targetNode?.y || 0 + 1; + const targetFieldY = targetNode?.y || 0 + 3; let targetFieldX = (targetNode?.x || 0) + 35 + targetTitleHeight; - if (!collapsedColumnsNodes[targetNode?.data.urn || 'no-op']) { + // if currentNode is a dataJob, draw line to center of data job + if (targetNode?.data.type === EntityType.DataJob) { + targetFieldX = targetNode?.x || 0; + } else if (!collapsedColumnsNodes[targetNode?.data.urn || 'no-op']) { if (!visibleColumnsByUrn[targetUrn]?.has(targetField)) { targetFieldX = (targetNode?.x || 0) + @@ -292,7 +295,10 @@ function layoutColumnTree( const sourceFieldY = currentNode?.y || 0 + 1; let sourceFieldX = (currentNode?.x || 0) + 30 + sourceTitleHeight; - if (!collapsedColumnsNodes[currentNode?.data.urn || 'no-op']) { + // if currentNode is a dataJob, draw line from center of data job + if (currentNode?.data.type === EntityType.DataJob) { + sourceFieldX = currentNode?.x || 0; + } else if (!collapsedColumnsNodes[currentNode?.data.urn || 'no-op']) { if (!visibleColumnsByUrn[entityUrn]?.has(sourceField)) { sourceFieldX = (currentNode?.x || 0) + diff --git a/datahub-web-react/src/app/permissions/policy/ManagePolicies.tsx b/datahub-web-react/src/app/permissions/policy/ManagePolicies.tsx index 369e6a76cf7dd..08327d40a7165 100644 --- a/datahub-web-react/src/app/permissions/policy/ManagePolicies.tsx +++ b/datahub-web-react/src/app/permissions/policy/ManagePolicies.tsx @@ -109,6 +109,7 @@ const toPolicyInput = (policy: Omit): PolicyUpdateInput => { allUsers: policy.actors.allUsers, allGroups: policy.actors.allGroups, resourceOwners: policy.actors.resourceOwners, + resourceOwnersTypes: policy.actors.resourceOwnersTypes, }, }; if (policy.resources !== null && policy.resources !== undefined) { diff --git a/datahub-web-react/src/app/permissions/policy/PolicyActorForm.tsx b/datahub-web-react/src/app/permissions/policy/PolicyActorForm.tsx index 785d0226dfe19..31b9472a7e53b 100644 --- a/datahub-web-react/src/app/permissions/policy/PolicyActorForm.tsx +++ b/datahub-web-react/src/app/permissions/policy/PolicyActorForm.tsx @@ -1,10 +1,12 @@ import React from 'react'; import { Form, Select, Switch, Tag, Typography } from 'antd'; import styled from 'styled-components'; +import { Maybe } from 'graphql/jsutils/Maybe'; import { useEntityRegistry } from '../../useEntityRegistry'; import { ActorFilter, CorpUser, EntityType, PolicyType, SearchResult } from '../../../types.generated'; import { useGetSearchResultsLazyQuery } from '../../../graphql/search.generated'; +import { useListOwnershipTypesQuery } from '../../../graphql/ownership.generated'; import { CustomAvatar } from '../../shared/avatar'; type Props = { @@ -36,6 +38,10 @@ const SearchResultContent = styled.div` align-items: center; `; +const OwnershipWrapper = styled.div` + margin-top: 12px; +`; + /** * Component used to construct the "actors" portion of a DataHub * access Policy by populating an ActorFilter object. @@ -46,12 +52,37 @@ export default function PolicyActorForm({ policyType, actors, setActors }: Props // Search for actors while building policy. const [userSearch, { data: userSearchData }] = useGetSearchResultsLazyQuery(); const [groupSearch, { data: groupSearchData }] = useGetSearchResultsLazyQuery(); - + const { data: ownershipData } = useListOwnershipTypesQuery({ + variables: { + input: {}, + }, + }); + const ownershipTypes = + ownershipData?.listOwnershipTypes?.ownershipTypes.filter((type) => type.urn !== 'urn:li:ownershipType:none') || + []; + const ownershipTypesMap = Object.fromEntries(ownershipTypes.map((type) => [type.urn, type.info?.name])); // Toggle the "Owners" switch const onToggleAppliesToOwners = (value: boolean) => { setActors({ ...actors, resourceOwners: value, + resourceOwnersTypes: value ? actors.resourceOwnersTypes : null, + }); + }; + + const onSelectOwnershipTypeActor = (newType: string) => { + const newResourceOwnersTypes: Maybe = [...(actors.resourceOwnersTypes || []), newType]; + setActors({ + ...actors, + resourceOwnersTypes: newResourceOwnersTypes, + }); + }; + + const onDeselectOwnershipTypeActor = (type: string) => { + const newResourceOwnersTypes: Maybe = actors.resourceOwnersTypes?.filter((u: string) => u !== type); + setActors({ + ...actors, + resourceOwnersTypes: newResourceOwnersTypes?.length ? newResourceOwnersTypes : null, }); }; @@ -175,6 +206,7 @@ export default function PolicyActorForm({ policyType, actors, setActors }: Props // Select dropdown values. const usersSelectValue = actors.allUsers ? ['All'] : actors.users || []; const groupsSelectValue = actors.allGroups ? ['All'] : actors.groups || []; + const ownershipTypesSelectValue = actors.resourceOwnersTypes || []; const tagRender = (props) => { // eslint-disable-next-line react/prop-types @@ -215,6 +247,36 @@ export default function PolicyActorForm({ policyType, actors, setActors }: Props selected privileges. + {actors.resourceOwners && ( + + + List of types of ownership which will be used to match owners. If empty, the policies + will applied to any type of ownership. + + + + )} )} Users}> diff --git a/datahub-web-react/src/app/permissions/policy/PolicyDetailsModal.tsx b/datahub-web-react/src/app/permissions/policy/PolicyDetailsModal.tsx index da599f53c3b50..68e91983babdb 100644 --- a/datahub-web-react/src/app/permissions/policy/PolicyDetailsModal.tsx +++ b/datahub-web-react/src/app/permissions/policy/PolicyDetailsModal.tsx @@ -103,6 +103,22 @@ export default function PolicyDetailsModal({ policy, visible, onClose, privilege ); }; + const resourceOwnersField = (actors) => { + if (!actors?.resourceOwners) { + return No; + } + if ((actors?.resolvedOwnershipTypes?.length ?? 0) > 0) { + return ( +
+ {actors?.resolvedOwnershipTypes?.map((type) => ( + {type.info.name} + ))} +
+ ); + } + return Yes - All owners; + }; + return ( @@ -180,7 +196,7 @@ export default function PolicyDetailsModal({ policy, visible, onClose, privilege
Applies to Owners - {policy?.actors?.resourceOwners ? 'True' : 'False'} + {resourceOwnersField(policy?.actors)}
Applies to Users diff --git a/datahub-web-react/src/app/search/SearchHeader.tsx b/datahub-web-react/src/app/search/SearchHeader.tsx index 7ca944c5c1d2e..496b320cd3e3c 100644 --- a/datahub-web-react/src/app/search/SearchHeader.tsx +++ b/datahub-web-react/src/app/search/SearchHeader.tsx @@ -9,9 +9,10 @@ import { AutoCompleteResultForEntity, EntityType } from '../../types.generated'; import EntityRegistry from '../entity/EntityRegistry'; import { ANTD_GRAY } from '../entity/shared/constants'; import { HeaderLinks } from '../shared/admin/HeaderLinks'; -import { useAppConfig } from '../useAppConfig'; +import { useAppConfig, useIsShowAcrylInfoEnabled } from '../useAppConfig'; import { DEFAULT_APP_CONFIG } from '../../appConfigContext'; import { ViewSelect } from '../entity/view/select/ViewSelect'; +import DemoButton from '../entity/shared/components/styled/DemoButton'; const { Header } = Layout; @@ -82,6 +83,7 @@ export const SearchHeader = ({ }: Props) => { const [isSearchBarFocused, setIsSearchBarFocused] = useState(false); const themeConfig = useTheme(); + const showAcrylInfo = useIsShowAcrylInfoEnabled(); const appConfig = useAppConfig(); const viewsEnabled = appConfig.config?.viewsConfig?.enabled; @@ -118,6 +120,7 @@ export const SearchHeader = ({ )} + {showAcrylInfo && } ); diff --git a/datahub-web-react/src/app/search/SearchPage.tsx b/datahub-web-react/src/app/search/SearchPage.tsx index ca50fe913101b..13a11bd1bd8dd 100644 --- a/datahub-web-react/src/app/search/SearchPage.tsx +++ b/datahub-web-react/src/app/search/SearchPage.tsx @@ -21,8 +21,9 @@ import { DownloadSearchResults, DownloadSearchResultsInput } from './utils/types import SearchFilters from './filters/SearchFilters'; import useGetSearchQueryInputs from './useGetSearchQueryInputs'; import useSearchFilterAnalytics from './filters/useSearchFilterAnalytics'; -import { useIsSearchV2, useSearchVersion } from './useSearchAndBrowseVersion'; +import { useIsBrowseV2, useIsSearchV2, useSearchVersion } from './useSearchAndBrowseVersion'; import useFilterMode from './filters/useFilterMode'; +import { useUpdateEducationStepIdsAllowlist } from '../onboarding/useUpdateEducationStepIdsAllowlist'; /** * A search results page. @@ -30,6 +31,7 @@ import useFilterMode from './filters/useFilterMode'; export const SearchPage = () => { const { trackClearAllFiltersEvent } = useSearchFilterAnalytics(); const showSearchFiltersV2 = useIsSearchV2(); + const showBrowseV2 = useIsBrowseV2(); const searchVersion = useSearchVersion(); const history = useHistory(); const { query, unionType, filters, orFilters, viewUrn, page, activeType } = useGetSearchQueryInputs(); @@ -167,6 +169,12 @@ export const SearchPage = () => { } }, [isSelectMode]); + // Render new search filters v2 onboarding step if the feature flag is on + useUpdateEducationStepIdsAllowlist(showSearchFiltersV2, SEARCH_RESULTS_FILTERS_V2_INTRO); + + // Render new browse v2 onboarding step if the feature flag is on + useUpdateEducationStepIdsAllowlist(showBrowseV2, SEARCH_RESULTS_BROWSE_SIDEBAR_ID); + return ( <> {!loading && ( diff --git a/datahub-web-react/src/app/search/SearchResultList.tsx b/datahub-web-react/src/app/search/SearchResultList.tsx index 591927cfb152d..a793348db6f1d 100644 --- a/datahub-web-react/src/app/search/SearchResultList.tsx +++ b/datahub-web-react/src/app/search/SearchResultList.tsx @@ -48,6 +48,7 @@ const ResultWrapper = styled.div<{ showUpdatedStyles: boolean }>` margin: 0 auto 8px auto; padding: 8px 16px; max-width: 1200px; + border-bottom: 1px solid ${ANTD_GRAY[5]}; `} `; diff --git a/datahub-web-react/src/app/search/SearchResults.tsx b/datahub-web-react/src/app/search/SearchResults.tsx index 29d81d2bcc6c3..61d5ba3cd98cd 100644 --- a/datahub-web-react/src/app/search/SearchResults.tsx +++ b/datahub-web-react/src/app/search/SearchResults.tsx @@ -23,17 +23,15 @@ import BrowseSidebar from './sidebar'; import ToggleSidebarButton from './ToggleSidebarButton'; import { SidebarProvider } from './sidebar/SidebarContext'; import { BrowseProvider } from './sidebar/BrowseContext'; -import analytics from '../analytics/analytics'; -import useToggle from '../shared/useToggle'; -import { EventType } from '../analytics'; import { useIsBrowseV2, useIsSearchV2 } from './useSearchAndBrowseVersion'; +import useToggleSidebar from './useToggleSidebar'; -const SearchResultsWrapper = styled.div<{ showUpdatedStyles: boolean }>` +const SearchResultsWrapper = styled.div<{ v2Styles: boolean }>` display: flex; flex: 1; ${(props) => - props.showUpdatedStyles && + props.v2Styles && ` overflow: hidden; `} @@ -47,12 +45,14 @@ const SearchBody = styled.div` overflow: auto; `; -const ResultContainer = styled.div<{ displayUpdatedStyles: boolean }>` +const ResultContainer = styled.div<{ v2Styles: boolean }>` flex: 1; overflow: auto; ${(props) => - props.displayUpdatedStyles + props.v2Styles ? ` + display: flex; + flex-direction: column; background-color: #F8F9FA; ` : ` @@ -66,7 +66,7 @@ const PaginationControlContainer = styled.div` text-align: center; `; -const PaginationInfoContainer = styled.div` +const PaginationInfoContainer = styled.div<{ v2Styles: boolean }>` padding-left: 24px; padding-right: 32px; height: 47px; @@ -75,6 +75,7 @@ const PaginationInfoContainer = styled.div` display: flex; justify-content: space-between; align-items: center; + ${({ v2Styles }) => v2Styles && `background-color: white;`} `; const LeftControlsContainer = styled.div` @@ -94,6 +95,15 @@ const StyledTabToolbar = styled(TabToolbar)` const SearchMenuContainer = styled.div``; +const SearchResultListContainer = styled.div<{ v2Styles: boolean }>` + ${({ v2Styles }) => + v2Styles && + ` + flex: 1; + overflow: auto; + `} +`; + interface Props { unionType?: UnionType; query: string; @@ -151,6 +161,7 @@ export const SearchResults = ({ }: Props) => { const showSearchFiltersV2 = useIsSearchV2(); const showBrowseV2 = useIsBrowseV2(); + const { isSidebarOpen, toggleSidebar } = useToggleSidebar(); const pageStart = searchResponse?.start || 0; const pageSize = searchResponse?.count || 0; const totalResults = searchResponse?.total || 0; @@ -161,19 +172,10 @@ export const SearchResults = ({ const searchResultUrns = combinedSiblingSearchResults.map((result) => result.entity.urn) || []; const selectedEntityUrns = selectedEntities.map((entity) => entity.urn); - const { isOpen: isSidebarOpen, toggle: toggleSidebar } = useToggle({ - initialValue: true, - onToggle: (isNowOpen: boolean) => - analytics.event({ - type: EventType.BrowseV2ToggleSidebarEvent, - action: isNowOpen ? 'open' : 'close', - }), - }); - return ( <> {loading && } - + {!showSearchFiltersV2 && (
@@ -194,8 +196,8 @@ export const SearchResults = ({ )} - - + + {showBrowseV2 && } @@ -233,7 +235,7 @@ export const SearchResults = ({ )} {(error && ) || (!loading && ( - <> + )} - + ))} diff --git a/datahub-web-react/src/app/search/ToggleSidebarButton.tsx b/datahub-web-react/src/app/search/ToggleSidebarButton.tsx index bdfc1e77d788a..e3c28aa2ad82c 100644 --- a/datahub-web-react/src/app/search/ToggleSidebarButton.tsx +++ b/datahub-web-react/src/app/search/ToggleSidebarButton.tsx @@ -1,6 +1,6 @@ -import React, { memo } from 'react'; +import React, { memo, useState } from 'react'; import Icon from '@ant-design/icons/lib/components/Icon'; -import { Button } from 'antd'; +import { Button, Tooltip } from 'antd'; import styled from 'styled-components'; import { ReactComponent as ExpandIcon } from '../../images/expand.svg'; import { ReactComponent as CollapseIcon } from '../../images/collapse.svg'; @@ -12,9 +12,37 @@ const ToggleIcon = styled(Icon)` } `; -const ToggleSidebarButton = ({ isOpen, onClick }: { isOpen: boolean; onClick: () => void }) => { +type Props = { + isOpen: boolean; + onClick: () => void; +}; + +const ToggleSidebarButton = ({ isOpen, onClick }: Props) => { + const [pauseTooltip, setPauseTooltip] = useState(false); + const title = isOpen ? 'Hide the navigation panel' : 'Open the navigation panel'; + const placement = isOpen ? 'bottom' : 'bottomRight'; + + const onClickButton = () => { + setPauseTooltip(true); + window.setTimeout(() => setPauseTooltip(false), 250); + onClick(); + }; + + const button = ( +