diff --git a/README.md b/README.md index 58764e2a789c9..22f57b6cbeb1d 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ HOSTED_DOCS_ONLY--> ## Introduction DataHub is an open-source metadata platform for the modern data stack. Read about the architectures of different metadata systems and why DataHub excels [here](https://engineering.linkedin.com/blog/2020/datahub-popular-metadata-architectures-explained). Also read our -[LinkedIn Engineering blog post](https://engineering.linkedin.com/blog/2019/data-hub), check out our [Strata presentation](https://speakerdeck.com/shirshanka/the-evolution-of-metadata-linkedins-journey-strata-nyc-2019) and watch our [Crunch Conference Talk](https://www.youtube.com/watch?v=OB-O0Y6OYDE). You should also visit [DataHub Architecture](docs/architecture/architecture.md) to get a better understanding of how DataHub is implemented and [DataHub Onboarding Guide](docs/modeling/extending-the-metadata-model.md) to understand how to extend DataHub for your own use cases. +[LinkedIn Engineering blog post](https://engineering.linkedin.com/blog/2019/data-hub), check out our [Strata presentation](https://speakerdeck.com/shirshanka/the-evolution-of-metadata-linkedins-journey-strata-nyc-2019) and watch our [Crunch Conference Talk](https://www.youtube.com/watch?v=OB-O0Y6OYDE). You should also visit [DataHub Architecture](docs/architecture/architecture.md) to get a better understanding of how DataHub is implemented. ## Quickstart @@ -99,6 +99,10 @@ Check out DataHub's [Features](docs/features.md) & [Roadmap](https://feature-req We welcome contributions from the community. Please refer to our [Contributing Guidelines](docs/CONTRIBUTING.md) for more details. We also have a [contrib](contrib) directory for incubating experimental features. +### Extending + +If you need to understand how to extend our model with custom types, please see [Extending the Metadata Model](docs/modeling/extending-the-metadata-model.md) + ## Community Join our [slack workspace](https://slack.datahubproject.io) for discussions and important announcements. You can also find out more about our upcoming [town hall meetings](docs/townhalls.md) and view past recordings. @@ -109,6 +113,7 @@ Here are the companies that have officially adopted DataHub. Please feel free to - [Adevinta](https://www.adevinta.com/) - [Banksalad](https://www.banksalad.com) +- [Cabify](https://cabify.tech/) - [DefinedCrowd](http://www.definedcrowd.com) - [DFDS](https://www.dfds.com/) - [Expedia Group](http://expedia.com) diff --git a/build.gradle b/build.gradle index 80dcdbe7f72ab..e4b86ee03507d 100644 --- a/build.gradle +++ b/build.gradle @@ -77,7 +77,8 @@ project.ext.externalDependency = [ 'javaxInject' : 'javax.inject:javax.inject:1', 'jerseyCore': 'org.glassfish.jersey.core:jersey-client:2.25.1', 'jerseyGuava': 'org.glassfish.jersey.bundles.repackaged:jersey-guava:2.25.1', - 'jettyJaas': 'org.eclipse.jetty:jetty-jaas:9.4.28.v20200408', + 'jettyJaas': 'org.eclipse.jetty:jetty-jaas:9.4.32.v20200930', + 'jgrapht': 'org.jgrapht:jgrapht-core:1.5.1', 'jsonSimple': 'com.googlecode.json-simple:json-simple:1.1.1', 'junitJupiterApi': "org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion", 'junitJupiterParams': "org.junit.jupiter:junit-jupiter-params:$junitJupiterVersion", @@ -98,6 +99,7 @@ project.ext.externalDependency = [ 'neo4jJavaDriver': 'org.neo4j.driver:neo4j-java-driver:4.0.1', 'opentelemetryApi': 'io.opentelemetry:opentelemetry-api:1.0.0', 'opentelemetryAnnotations': 'io.opentelemetry:opentelemetry-extension-annotations:1.0.0', + 'opentracingJdbc':'io.opentracing.contrib:opentracing-jdbc:0.2.15', 'parseqTest': 'com.linkedin.parseq:parseq:3.0.7:test', 'parquet': 'org.apache.parquet:parquet-avro:1.12.0', 'picocli': 'info.picocli:picocli:4.5.0', @@ -110,11 +112,12 @@ project.ext.externalDependency = [ 'pac4j': 'org.pac4j:pac4j-oidc:3.6.0', 'playPac4j': 'org.pac4j:play-pac4j_2.11:7.0.1', 'postgresql': 'org.postgresql:postgresql:42.3.3', + 'protobuf': 'com.google.protobuf:protobuf-java:3.19.3', 'reflections': 'org.reflections:reflections:0.9.9', 'resilience4j': 'io.github.resilience4j:resilience4j-retry:1.7.1', 'rythmEngine': 'org.rythmengine:rythm-engine:1.3.0', 'servletApi': 'javax.servlet:javax.servlet-api:3.1.0', - 'shiroCore': 'org.apache.shiro:shiro-core:1.7.1', + 'shiroCore': 'org.apache.shiro:shiro-core:1.8.0', 'sparkSql' : 'org.apache.spark:spark-sql_2.11:2.4.8', 'sparkHive' : 'org.apache.spark:spark-hive_2.11:2.4.8', 'springBeans': 'org.springframework:spring-beans:5.2.3.RELEASE', @@ -140,6 +143,7 @@ project.ext.externalDependency = [ 'typesafeConfig':'com.typesafe:config:1.4.1', 'wiremock':'com.github.tomakehurst:wiremock:2.10.0', 'zookeeper': 'org.apache.zookeeper:zookeeper:3.4.14' + ] allprojects { @@ -194,14 +198,27 @@ subprojects { } } - tasks.withType(JavaCompile).configureEach { - javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(8) + if (project.name != 'datahub-protobuf') { + tasks.withType(JavaCompile).configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(8) + } } - } - tasks.withType(Test).configureEach { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(8) + tasks.withType(Test).configureEach { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(8) + } + } + } else { + tasks.withType(JavaCompile).configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(11) + } + } + tasks.withType(Test).configureEach { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(11) + } } } diff --git a/datahub-frontend/app/auth/sso/oidc/OidcCallbackLogic.java b/datahub-frontend/app/auth/sso/oidc/OidcCallbackLogic.java index 22026681dde19..23776ff8f31e7 100644 --- a/datahub-frontend/app/auth/sso/oidc/OidcCallbackLogic.java +++ b/datahub-frontend/app/auth/sso/oidc/OidcCallbackLogic.java @@ -55,6 +55,7 @@ import play.mvc.Result; import auth.sso.SsoManager; +import static com.linkedin.metadata.Constants.*; import static play.mvc.Results.*; import static auth.AuthUtils.*; @@ -125,16 +126,14 @@ private Result handleOidcCallback( if (oidcConfigs.isJitProvisioningEnabled()) { log.debug("Just-in-time provisioning is enabled. Beginning provisioning process..."); CorpUserSnapshot extractedUser = extractUser(corpUserUrn, profile); + tryProvisionUser(extractedUser); if (oidcConfigs.isExtractGroupsEnabled()) { // Extract groups & provision them. List extractedGroups = extractGroups(profile); tryProvisionGroups(extractedGroups); - if (extractedGroups.size() > 0) { - // Associate group with the user logging in. - extractedUser.getAspects().add(CorpUserAspect.create(createGroupMembership(extractedGroups))); - } + // Add users to groups on DataHub. Note that this clears existing group membership for a user if it already exists. + updateGroupMembership(corpUserUrn, createGroupMembership(extractedGroups)); } - tryProvisionUser(extractedUser); } else if (oidcConfigs.isPreProvisioningRequired()) { // We should only allow logins for user accounts that have been pre-provisioned log.debug("Pre Provisioning is required. Beginning validation of extracted user..."); @@ -372,6 +371,21 @@ private void tryProvisionGroups(List corpGroups) { } } + private void updateGroupMembership(Urn urn, GroupMembership groupMembership) { + log.debug(String.format("Updating group membership for user %s", urn)); + final MetadataChangeProposal proposal = new MetadataChangeProposal(); + proposal.setEntityUrn(urn); + proposal.setEntityType(CORP_USER_ENTITY_NAME); + proposal.setAspectName(GROUP_MEMBERSHIP_ASPECT_NAME); + proposal.setAspect(GenericAspectUtils.serializeAspect(groupMembership)); + proposal.setChangeType(ChangeType.UPSERT); + try { + _entityClient.ingestProposal(proposal, _systemAuthentication); + } catch (RemoteInvocationException e) { + throw new RuntimeException(String.format("Failed to update group membership for user with urn %s", urn), e); + } + } + private void verifyPreProvisionedUser(CorpuserUrn urn) { // Validate that the user exists in the system (there is more than just a key aspect for them, as of today). try { diff --git a/datahub-frontend/app/auth/sso/oidc/OidcConfigs.java b/datahub-frontend/app/auth/sso/oidc/OidcConfigs.java index e0b354e770ba8..1bf58572ad570 100644 --- a/datahub-frontend/app/auth/sso/oidc/OidcConfigs.java +++ b/datahub-frontend/app/auth/sso/oidc/OidcConfigs.java @@ -47,7 +47,7 @@ public class OidcConfigs extends SsoConfigs { private static final String DEFAULT_OIDC_CLIENT_AUTHENTICATION_METHOD = "client_secret_basic"; private static final String DEFAULT_OIDC_JIT_PROVISIONING_ENABLED = "true"; private static final String DEFAULT_OIDC_PRE_PROVISIONING_REQUIRED = "false"; - private static final String DEFAULT_OIDC_EXTRACT_GROUPS_ENABLED = "true"; + private static final String DEFAULT_OIDC_EXTRACT_GROUPS_ENABLED = "false"; // False since extraction of groups can overwrite existing group membership. private static final String DEFAULT_OIDC_GROUPS_CLAIM = "groups"; private String clientId; 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 4e381201f9cb3..d33d9744a02c2 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 @@ -12,6 +12,8 @@ import com.linkedin.datahub.graphql.generated.AggregationMetadata; import com.linkedin.datahub.graphql.generated.Aspect; import com.linkedin.datahub.graphql.generated.Assertion; +import com.linkedin.datahub.graphql.generated.AutoCompleteResultForEntity; +import com.linkedin.datahub.graphql.generated.AutoCompleteResults; import com.linkedin.datahub.graphql.generated.BrowseResults; import com.linkedin.datahub.graphql.generated.Chart; import com.linkedin.datahub.graphql.generated.ChartInfo; @@ -514,6 +516,10 @@ private void configureContainerResolvers(final RuntimeWiring.Builder builder) { .type("Container", typeWiring -> typeWiring .dataFetcher("relationships", new EntityRelationshipsResultResolver(graphClient)) .dataFetcher("entities", new ContainerEntitiesResolver(entityClient)) + .dataFetcher("domain", new LoadableTypeResolver<>(domainType, (env) -> { + final Container container = env.getSource(); + return container.getDomain() != null ? container.getDomain().getUrn() : null; + })) .dataFetcher("platform", new LoadableTypeResolver<>(dataPlatformType, (env) -> ((Container) env.getSource()).getPlatform().getUrn())) @@ -730,7 +736,20 @@ private void configureGenericEntityResolvers(final RuntimeWiring.Builder builder (env) -> ((ListDomainsResult) env.getSource()).getDomains().stream() .map(Domain::getUrn) .collect(Collectors.toList()))) + ) + .type("AutoCompleteResults", typeWiring -> typeWiring + .dataFetcher("entities", + new EntityTypeBatchResolver( + new ArrayList<>(entityTypes), + (env) -> ((AutoCompleteResults) env.getSource()).getEntities())) + ) + .type("AutoCompleteResultForEntity", typeWiring -> typeWiring + .dataFetcher("entities", + new EntityTypeBatchResolver( + new ArrayList<>(entityTypes), + (env) -> ((AutoCompleteResultForEntity) env.getSource()).getEntities())) ); + ; } /** diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/analytics/resolver/GetChartsResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/analytics/resolver/GetChartsResolver.java index 63592489b4959..c357af8bcfe69 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/analytics/resolver/GetChartsResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/analytics/resolver/GetChartsResolver.java @@ -75,7 +75,7 @@ private List getProductAnalyticsCharts(Authentication authentica final List wauTimeseries = _analyticsService.getTimeseriesChart(_analyticsService.getUsageIndexName(), twoMonthsDateRange, weeklyInterval, - Optional.empty(), ImmutableMap.of(), Optional.of("browserId")); + Optional.empty(), ImmutableMap.of(), Collections.emptyMap(), Optional.of("browserId")); charts.add(TimeSeriesChart.builder() .setTitle(wauTitle) .setDateRange(twoMonthsDateRange) @@ -90,7 +90,8 @@ private List getProductAnalyticsCharts(Authentication authentica final List searchesTimeseries = _analyticsService.getTimeseriesChart(_analyticsService.getUsageIndexName(), lastWeekDateRange, dailyInterval, - Optional.empty(), ImmutableMap.of("type", ImmutableList.of(searchEventType)), Optional.empty()); + Optional.empty(), ImmutableMap.of("type", ImmutableList.of(searchEventType)), Collections.emptyMap(), + Optional.empty()); charts.add(TimeSeriesChart.builder() .setTitle(searchesTitle) .setDateRange(lastWeekDateRange) @@ -104,8 +105,8 @@ private List getProductAnalyticsCharts(Authentication authentica final List topSearchQueries = _analyticsService.getTopNTableChart(_analyticsService.getUsageIndexName(), Optional.of(lastWeekDateRange), - "query.keyword", ImmutableMap.of("type", ImmutableList.of(searchEventType)), Optional.empty(), 10, - AnalyticsUtil::buildCellWithSearchLandingPage); + "query.keyword", ImmutableMap.of("type", ImmutableList.of(searchEventType)), Collections.emptyMap(), + Optional.empty(), 10, AnalyticsUtil::buildCellWithSearchLandingPage); charts.add(TableChart.builder().setTitle(topSearchTitle).setColumns(columns).setRows(topSearchQueries).build()); // Chart 4: Bar Graph Chart @@ -113,7 +114,8 @@ private List getProductAnalyticsCharts(Authentication authentica final List sectionViewsPerEntityType = _analyticsService.getBarChart(_analyticsService.getUsageIndexName(), Optional.of(lastWeekDateRange), ImmutableList.of("entityType.keyword", "section.keyword"), - ImmutableMap.of("type", ImmutableList.of("EntitySectionViewEvent")), Optional.empty(), true); + ImmutableMap.of("type", ImmutableList.of("EntitySectionViewEvent")), Collections.emptyMap(), + Optional.empty(), true); charts.add(BarChart.builder().setTitle(sectionViewsTitle).setBars(sectionViewsPerEntityType).build()); // Chart 5: Bar Graph Chart @@ -121,7 +123,8 @@ private List getProductAnalyticsCharts(Authentication authentica final List eventsByEventType = _analyticsService.getBarChart(_analyticsService.getUsageIndexName(), Optional.of(lastWeekDateRange), ImmutableList.of("entityType.keyword", "actionType.keyword"), - ImmutableMap.of("type", ImmutableList.of("EntityActionEvent")), Optional.empty(), true); + ImmutableMap.of("type", ImmutableList.of("EntityActionEvent")), Collections.emptyMap(), Optional.empty(), + true); charts.add(BarChart.builder().setTitle(actionsByTypeTitle).setBars(eventsByEventType).build()); // Chart 6: Table Chart @@ -131,7 +134,7 @@ private List getProductAnalyticsCharts(Authentication authentica final List topViewedDatasets = _analyticsService.getTopNTableChart(_analyticsService.getUsageIndexName(), Optional.of(lastWeekDateRange), "entityUrn.keyword", ImmutableMap.of("type", ImmutableList.of("EntityViewEvent"), "entityType.keyword", - ImmutableList.of(EntityType.DATASET.name())), Optional.empty(), 10, + ImmutableList.of(EntityType.DATASET.name())), Collections.emptyMap(), Optional.empty(), 10, AnalyticsUtil::buildCellWithEntityLandingPage); AnalyticsUtil.hydrateDisplayNameForTable(_entityClient, topViewedDatasets, Constants.DATASET_ENTITY_NAME, ImmutableSet.of(Constants.DATASET_KEY_ASPECT_NAME), AnalyticsUtil::getDatasetName, authentication); @@ -145,7 +148,8 @@ private List getGlobalMetadataAnalyticsCharts(Authentication aut // Chart 1: Entities per domain final List entitiesPerDomain = _analyticsService.getBarChart(_analyticsService.getAllEntityIndexName(), Optional.empty(), - ImmutableList.of("domains.keyword", "platform.keyword"), Collections.emptyMap(), Optional.empty(), false); + ImmutableList.of("domains.keyword", "platform.keyword"), Collections.emptyMap(), + ImmutableMap.of("removed", ImmutableList.of("true")), Optional.empty(), false); AnalyticsUtil.hydrateDisplayNameForBars(_entityClient, entitiesPerDomain, Constants.DOMAIN_ENTITY_NAME, ImmutableSet.of(Constants.DOMAIN_PROPERTIES_ASPECT_NAME), AnalyticsUtil::getDomainName, authentication); AnalyticsUtil.hydrateDisplayNameForSegments(_entityClient, entitiesPerDomain, Constants.DATA_PLATFORM_ENTITY_NAME, @@ -157,7 +161,8 @@ private List getGlobalMetadataAnalyticsCharts(Authentication aut // Chart 2: Entities per platform final List entitiesPerPlatform = _analyticsService.getBarChart(_analyticsService.getAllEntityIndexName(), Optional.empty(), - ImmutableList.of("platform.keyword"), Collections.emptyMap(), Optional.empty(), false); + ImmutableList.of("platform.keyword"), Collections.emptyMap(), + ImmutableMap.of("removed", ImmutableList.of("true")), Optional.empty(), false); AnalyticsUtil.hydrateDisplayNameForBars(_entityClient, entitiesPerPlatform, Constants.DATA_PLATFORM_ENTITY_NAME, ImmutableSet.of(Constants.DATA_PLATFORM_INFO_ASPECT_NAME), AnalyticsUtil::getPlatformName, authentication); if (!entitiesPerPlatform.isEmpty()) { @@ -167,7 +172,8 @@ private List getGlobalMetadataAnalyticsCharts(Authentication aut // Chart 3: Entities per term final List entitiesPerTerm = _analyticsService.getBarChart(_analyticsService.getAllEntityIndexName(), Optional.empty(), - ImmutableList.of("glossaryTerms.keyword"), Collections.emptyMap(), Optional.empty(), false); + ImmutableList.of("glossaryTerms.keyword"), Collections.emptyMap(), + ImmutableMap.of("removed", ImmutableList.of("true")), Optional.empty(), false); AnalyticsUtil.hydrateDisplayNameForBars(_entityClient, entitiesPerTerm, Constants.GLOSSARY_TERM_ENTITY_NAME, ImmutableSet.of(Constants.GLOSSARY_TERM_KEY_ASPECT_NAME), AnalyticsUtil::getTermName, authentication); if (!entitiesPerTerm.isEmpty()) { @@ -177,7 +183,8 @@ private List getGlobalMetadataAnalyticsCharts(Authentication aut // Chart 4: Entities per fabric type final List entitiesPerEnv = _analyticsService.getBarChart(_analyticsService.getAllEntityIndexName(), Optional.empty(), - ImmutableList.of("origin.keyword"), Collections.emptyMap(), Optional.empty(), false); + ImmutableList.of("origin.keyword"), Collections.emptyMap(), + ImmutableMap.of("removed", ImmutableList.of("true")), Optional.empty(), false); if (entitiesPerEnv.size() > 1) { charts.add(BarChart.builder().setTitle("Entities per Environment").setBars(entitiesPerEnv).build()); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/analytics/service/AnalyticsService.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/analytics/service/AnalyticsService.java index 17092234a8e91..48c017cdcaefd 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/analytics/service/AnalyticsService.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/analytics/service/AnalyticsService.java @@ -1,7 +1,6 @@ package com.linkedin.datahub.graphql.analytics.service; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.linkedin.datahub.graphql.generated.BarSegment; import com.linkedin.datahub.graphql.generated.Cell; import com.linkedin.datahub.graphql.generated.DateInterval; @@ -75,14 +74,14 @@ public String getUsageIndexName() { public List getTimeseriesChart(String indexName, DateRange dateRange, DateInterval granularity, Optional dimension, // Length 1 for now - Map> filters, Optional uniqueOn) { + Map> filters, Map> mustNotFilters, Optional uniqueOn) { log.debug( String.format("Invoked getTimeseriesChart with indexName: %s, dateRange: %s, granularity: %s, dimension: %s,", indexName, dateRange, granularity, dimension) + String.format("filters: %s, uniqueOn: %s", filters, uniqueOn)); - AggregationBuilder filteredAgg = getFilteredAggregation(filters, ImmutableMap.of(), Optional.of(dateRange)); + AggregationBuilder filteredAgg = getFilteredAggregation(filters, mustNotFilters, Optional.of(dateRange)); AggregationBuilder dateHistogram = AggregationBuilders.dateHistogram(DATE_HISTOGRAM) .field("timestamp") @@ -128,13 +127,14 @@ private List extractPointsFromAggregations(Aggregations aggreg public List getBarChart(String indexName, Optional dateRange, List dimensions, // Length 1 or 2 - Map> filters, Optional uniqueOn, boolean showMissing) { + Map> filters, Map> mustNotFilters, Optional uniqueOn, + boolean showMissing) { log.debug( String.format("Invoked getBarChart with indexName: %s, dateRange: %s, dimensions: %s,", indexName, dateRange, dimensions) + String.format("filters: %s, uniqueOn: %s", filters, uniqueOn)); assert (dimensions.size() == 1 || dimensions.size() == 2); - AggregationBuilder filteredAgg = getFilteredAggregation(filters, ImmutableMap.of(), dateRange); + AggregationBuilder filteredAgg = getFilteredAggregation(filters, mustNotFilters, dateRange); TermsAggregationBuilder termAgg = AggregationBuilders.terms(DIMENSION).field(dimensions.get(0)); if (showMissing) { @@ -142,8 +142,7 @@ public List getBarChart(String indexName, Optional dateRang } if (dimensions.size() == 2) { - TermsAggregationBuilder secondTermAgg = - AggregationBuilders.terms(SECOND_DIMENSION).field(dimensions.get(1)); + TermsAggregationBuilder secondTermAgg = AggregationBuilders.terms(SECOND_DIMENSION).field(dimensions.get(1)); if (showMissing) { secondTermAgg.missing(NA); } @@ -194,13 +193,13 @@ public Row buildRow(String groupByValue, Function groupByValueToCe } public List getTopNTableChart(String indexName, Optional dateRange, String groupBy, - Map> filters, Optional uniqueOn, int maxRows, - Function groupByValueToCell) { + Map> filters, Map> mustNotFilters, Optional uniqueOn, + int maxRows, Function groupByValueToCell) { log.debug( String.format("Invoked getTopNTableChart with indexName: %s, dateRange: %s, groupBy: %s", indexName, dateRange, groupBy) + String.format("filters: %s, uniqueOn: %s", filters, uniqueOn)); - AggregationBuilder filteredAgg = getFilteredAggregation(filters, ImmutableMap.of(), dateRange); + AggregationBuilder filteredAgg = getFilteredAggregation(filters, mustNotFilters, dateRange); TermsAggregationBuilder termAgg = AggregationBuilders.terms(DIMENSION).field(groupBy).size(maxRows); if (uniqueOn.isPresent()) { diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/AddOwnerResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/AddOwnerResolver.java index 097edcfceb75c..2e5ebbd3c6f6c 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/AddOwnerResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/AddOwnerResolver.java @@ -7,6 +7,7 @@ import com.linkedin.datahub.graphql.exception.AuthorizationException; import com.linkedin.datahub.graphql.generated.AddOwnerInput; import com.linkedin.datahub.graphql.generated.OwnerEntityType; +import com.linkedin.datahub.graphql.generated.OwnershipType; import com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils; import com.linkedin.metadata.entity.EntityService; import graphql.schema.DataFetcher; @@ -30,6 +31,7 @@ public CompletableFuture get(DataFetchingEnvironment environment) throw Urn ownerUrn = Urn.createFromString(input.getOwnerUrn()); OwnerEntityType ownerEntityType = input.getOwnerEntityType(); + OwnershipType type = input.getType() == null ? OwnershipType.NONE : input.getType(); Urn targetUrn = Urn.createFromString(input.getResourceUrn()); if (!OwnerUtils.isAuthorizedToUpdateOwners(environment.getContext(), targetUrn)) { @@ -50,6 +52,8 @@ public CompletableFuture get(DataFetchingEnvironment environment) throw Urn actor = CorpuserUrn.createFromString(((QueryContext) environment.getContext()).getActorUrn()); OwnerUtils.addOwner( ownerUrn, + // Assumption Alert: Assumes that GraphQL ownership type === GMS ownership type + com.linkedin.common.OwnershipType.valueOf(type.name()), targetUrn, actor, _entityService 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 134f36d920ea3..d7b9a3ec370e8 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 @@ -18,6 +18,7 @@ import com.linkedin.metadata.Constants; import com.linkedin.metadata.authorization.PoliciesConfig; import com.linkedin.metadata.entity.EntityService; +import java.util.stream.Collectors; import javax.annotation.Nonnull; import lombok.extern.slf4j.Slf4j; @@ -34,6 +35,7 @@ private OwnerUtils() { } public static void addOwner( Urn ownerUrn, + OwnershipType type, Urn resourceUrn, Urn actor, EntityService entityService @@ -43,7 +45,7 @@ public static void addOwner( Constants.OWNERSHIP_ASPECT_NAME, entityService, new Ownership()); - addOwner(ownershipAspect, ownerUrn); + addOwner(ownershipAspect, ownerUrn, type); persistAspect(resourceUrn, Constants.OWNERSHIP_ASPECT_NAME, ownershipAspect, actor, entityService); } @@ -63,23 +65,22 @@ public static void removeOwner( persistAspect(resourceUrn, Constants.OWNERSHIP_ASPECT_NAME, ownershipAspect, actor, entityService); } - private static void addOwner(Ownership ownershipAspect, Urn ownerUrn) { + private static void addOwner(Ownership ownershipAspect, Urn ownerUrn, OwnershipType type) { if (!ownershipAspect.hasOwners()) { ownershipAspect.setOwners(new OwnerArray()); } - OwnerArray ownerArray = ownershipAspect.getOwners(); - - // if owner exists, do not add it again - if (ownerArray.stream().anyMatch(association -> association.getOwner().equals(ownerUrn))) { - return; - } + final OwnerArray ownerArray = new OwnerArray(ownershipAspect.getOwners() + .stream() + .filter(owner -> !owner.getOwner().equals(ownerUrn)) + .collect(Collectors.toList())); Owner newOwner = new Owner(); - newOwner.setType(OwnershipType.DATAOWNER); + newOwner.setType(type); newOwner.setSource(new OwnershipSource().setType(OwnershipSourceType.MANUAL)); newOwner.setOwner(ownerUrn); ownerArray.add(newOwner); + ownershipAspect.setOwners(ownerArray); } private static void removeOwner(Ownership ownership, Urn ownerUrn) { diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/AutoCompleteForMultipleResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/AutoCompleteForMultipleResolver.java index 2c1292b03d635..911be3b9f2814 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/AutoCompleteForMultipleResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/AutoCompleteForMultipleResolver.java @@ -18,6 +18,7 @@ import org.slf4j.LoggerFactory; import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; +import static com.linkedin.datahub.graphql.resolvers.search.SearchUtils.*; import static org.apache.commons.lang3.StringUtils.isBlank; /** @@ -27,11 +28,9 @@ public class AutoCompleteForMultipleResolver implements DataFetcher> _searchableEntities; private final Map> _typeToEntity; public AutoCompleteForMultipleResolver(@Nonnull final List> searchableEntities) { - _searchableEntities = searchableEntities; _typeToEntity = searchableEntities.stream().collect(Collectors.toMap( SearchableEntityType::type, entity -> entity @@ -51,10 +50,18 @@ public CompletableFuture get(DataFetchingEnvironmen List types = input.getTypes(); if (types != null && types.size() > 0) { - return AutocompleteUtils.batchGetAutocompleteResults(types.stream().map(type -> _typeToEntity.get(type)).collect( - Collectors.toList()), sanitizedQuery, input, environment); + return AutocompleteUtils.batchGetAutocompleteResults( + types.stream().map(_typeToEntity::get).collect(Collectors.toList()), + sanitizedQuery, + input, + environment); } - return AutocompleteUtils.batchGetAutocompleteResults(_searchableEntities, sanitizedQuery, input, environment); + // By default, autocomplete only against the set of Searchable Entity Types. + return AutocompleteUtils.batchGetAutocompleteResults( + AUTO_COMPLETE_ENTITY_TYPES.stream().map(_typeToEntity::get).collect(Collectors.toList()), + sanitizedQuery, + input, + environment); } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/AutocompleteUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/AutocompleteUtils.java index 3fdeeadc2ed96..99e81cefd99b7 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/AutocompleteUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/AutocompleteUtils.java @@ -7,7 +7,7 @@ import com.linkedin.datahub.graphql.types.SearchableEntityType; import graphql.schema.DataFetchingEnvironment; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @@ -30,35 +30,35 @@ public static CompletableFuture batchGetAutocomplet ) { final int limit = input.getLimit() != null ? input.getLimit() : DEFAULT_LIMIT; - final CompletableFuture[] autoCompletesFuture = entities.stream().map(entity -> { - return CompletableFuture.supplyAsync(() -> { - try { - final AutoCompleteResults searchResult = entity.autoComplete( - sanitizedQuery, - input.getField(), - input.getFilters(), - limit, - environment.getContext() - ); - final AutoCompleteResultForEntity autoCompleteResultForEntity = - new AutoCompleteResultForEntity(entity.type(), searchResult.getSuggestions()); - return autoCompleteResultForEntity; - } catch (Exception e) { - _logger.error("Failed to execute autocomplete all: " - + String.format("field %s, query %s, filters: %s, limit: %s", - input.getField(), - input.getQuery(), - input.getFilters(), - input.getLimit()) + " " - + e.getMessage()); - return new AutoCompleteResultForEntity(entity.type(), new ArrayList<>()); - } - }); - }).toArray(CompletableFuture[]::new); - return CompletableFuture.allOf(autoCompletesFuture) + final List> autoCompletesFuture = entities.stream().map(entity -> CompletableFuture.supplyAsync(() -> { + try { + final AutoCompleteResults searchResult = entity.autoComplete( + sanitizedQuery, + input.getField(), + input.getFilters(), + limit, + environment.getContext() + ); + return new AutoCompleteResultForEntity( + entity.type(), + searchResult.getSuggestions(), + searchResult.getEntities() + ); + } catch (Exception e) { + _logger.error("Failed to execute autocomplete all: " + + String.format("field %s, query %s, filters: %s, limit: %s", + input.getField(), + input.getQuery(), + input.getFilters(), + input.getLimit()) + " " + + e.getMessage()); + return new AutoCompleteResultForEntity(entity.type(), Collections.emptyList(), Collections.emptyList()); + } + })).collect(Collectors.toList()); + return CompletableFuture.allOf(autoCompletesFuture.toArray(new CompletableFuture[0])) .thenApplyAsync((res) -> { AutoCompleteMultipleResults result = new AutoCompleteMultipleResults(sanitizedQuery, new ArrayList<>()); - result.setSuggestions(Arrays.stream(autoCompletesFuture) + result.setSuggestions(autoCompletesFuture.stream() .map(CompletableFuture::join) .filter( autoCompleteResultForEntity -> 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 398cea0c50f84..0153f5c39561c 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 @@ -9,9 +9,41 @@ public class SearchUtils { private SearchUtils() { } + /** + * Entities that are searched by default in Search Across Entities + */ public static final List SEARCHABLE_ENTITY_TYPES = - ImmutableList.of(EntityType.DATASET, EntityType.DASHBOARD, EntityType.CHART, EntityType.MLMODEL, - EntityType.MLMODEL_GROUP, EntityType.MLFEATURE_TABLE, EntityType.DATA_FLOW, EntityType.DATA_JOB, - EntityType.GLOSSARY_TERM, EntityType.TAG, EntityType.CORP_USER, EntityType.CORP_GROUP, EntityType.CONTAINER, + ImmutableList.of( + EntityType.DATASET, + EntityType.DASHBOARD, + EntityType.CHART, + EntityType.MLMODEL, + EntityType.MLMODEL_GROUP, + EntityType.MLFEATURE_TABLE, + EntityType.DATA_FLOW, + EntityType.DATA_JOB, + EntityType.GLOSSARY_TERM, + EntityType.TAG, + EntityType.CORP_USER, + EntityType.CORP_GROUP, + EntityType.CONTAINER, EntityType.DOMAIN); + + /** + * Entities that are part of autocomplete by default in Auto Complete Across Entities + */ + public static final List AUTO_COMPLETE_ENTITY_TYPES = + ImmutableList.of( + EntityType.DATASET, + EntityType.DASHBOARD, + EntityType.CHART, + EntityType.MLMODEL, + EntityType.MLMODEL_GROUP, + EntityType.MLFEATURE_TABLE, + EntityType.DATA_FLOW, + EntityType.DATA_JOB, + EntityType.GLOSSARY_TERM, + EntityType.TAG, + EntityType.CORP_USER, + EntityType.CORP_GROUP); } 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 53beee6da93e1..1fc30a0234823 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 @@ -27,6 +27,8 @@ import com.linkedin.metadata.Constants; import javax.annotation.Nullable; +import static com.linkedin.metadata.Constants.*; + public class ContainerMapper { @@ -43,8 +45,9 @@ public static Container map(final EntityResponse entityResponse) { if (envelopedPlatformInstance != null) { result.setPlatform(mapPlatform(new DataPlatformInstance(envelopedPlatformInstance.getValue().data()))); } else { - // Containers must have DPI to be rendered. - return null; + final DataPlatform unknownPlatform = new DataPlatform(); + unknownPlatform.setUrn(UNKNOWN_DATA_PLATFORM); + result.setPlatform(unknownPlatform); } final EnvelopedAspect envelopedContainerProperties = aspects.get(Constants.CONTAINER_PROPERTIES_ASPECT_NAME); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/AutoCompleteResultsMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/AutoCompleteResultsMapper.java index ea94173f73668..d575a81f4ae03 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/AutoCompleteResultsMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/AutoCompleteResultsMapper.java @@ -1,8 +1,10 @@ package com.linkedin.datahub.graphql.types.mappers; import com.linkedin.datahub.graphql.generated.AutoCompleteResults; +import com.linkedin.datahub.graphql.types.common.mappers.UrnToEntityMapper; import com.linkedin.metadata.query.AutoCompleteResult; +import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -19,6 +21,8 @@ public AutoCompleteResults apply(@Nonnull final AutoCompleteResult input) { final AutoCompleteResults result = new AutoCompleteResults(); result.setQuery(input.getQuery()); result.setSuggestions(input.getSuggestions()); + result.setEntities(input.getEntities().stream().map(entity -> UrnToEntityMapper.map(entity.getUrn())).collect( + Collectors.toList())); return result; } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/tag/mappers/TagUpdateInputMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/tag/mappers/TagUpdateInputMapper.java index a09a2e4693018..264f4041f88cf 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/tag/mappers/TagUpdateInputMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/tag/mappers/TagUpdateInputMapper.java @@ -47,7 +47,7 @@ public Collection apply( final Ownership ownership = new Ownership(); final Owner owner = new Owner(); owner.setOwner(actor); - owner.setType(OwnershipType.DATAOWNER); + owner.setType(OwnershipType.NONE); owner.setSource(new OwnershipSource().setType(OwnershipSourceType.SERVICE)); ownership.setOwners(new OwnerArray(owner)); ownership.setLastModified(auditStamp); diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index 7e38e71079c94..d2f43b5bf9256 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -2147,34 +2147,60 @@ Note that this field will soon become deprecated due to low usage """ enum OwnershipType { """ - A person or group that is in charge of developing the code + A person or group who is responsible for technical aspects of the asset. + """ + TECHNICAL_OWNER + """ - DEVELOPER + A person or group who is responsible for logical, or business related, aspects of the asset. + """ + BUSINESS_OWNER """ - A person or group that is owning the data + A steward, expert, or delegate responsible for the asset. """ - DATAOWNER + DATA_STEWARD + + """ + No specific type associated with the owner. + """ + NONE + + """ + A person or group that owns the data. + Deprecated! This ownership type is no longer supported. Use TECHNICAL_OWNER instead. + """ + DATAOWNER @deprecated + + """ + A person or group that is in charge of developing the code + Deprecated! This ownership type is no longer supported. Use TECHNICAL_OWNER instead. + """ + DEVELOPER @deprecated """ A person or a group that overseas the operation, eg a DBA or SRE + Deprecated! This ownership type is no longer supported. Use TECHNICAL_OWNER instead. """ - DELEGATE + DELEGATE @deprecated """ A person, group, or service that produces or generates the data + Deprecated! This ownership type is no longer supported. Use TECHNICAL_OWNER instead. """ - PRODUCER + PRODUCER @deprecated """ - A person, group, or service that consumes the data + A person or a group that has direct business interest + Deprecated! Use BUSINESS_OWNER instead. """ - CONSUMER + STAKEHOLDER @deprecated """ - A person or a group that has direct business interest + A person, group, or service that consumes the data + Deprecated! This ownership type is no longer supported. """ - STAKEHOLDER + CONSUMER @deprecated } """ @@ -5105,6 +5131,11 @@ input AddOwnerInput { """ ownerEntityType: OwnerEntityType! + """ + The ownership type for the new owner. If none is provided, then a new NONE will be added. + """ + type: OwnershipType + """ The urn of the resource or entity to attach or remove the owner from, for example a dataset urn """ diff --git a/datahub-graphql-core/src/main/resources/recommendation.graphql b/datahub-graphql-core/src/main/resources/recommendation.graphql index 8b522a14f63c1..5f1340ba45254 100644 --- a/datahub-graphql-core/src/main/resources/recommendation.graphql +++ b/datahub-graphql-core/src/main/resources/recommendation.graphql @@ -42,7 +42,12 @@ enum ScenarioType { """ Recommendations to show on an Entity Profile page """ - ENTITY_PROFILE + ENTITY_PROFILE, + + """ + Recommendations to show on the search bar when clicked + """ + SEARCH_BAR } """ diff --git a/datahub-graphql-core/src/main/resources/search.graphql b/datahub-graphql-core/src/main/resources/search.graphql index 4d59332ae5748..b5deed8a35109 100644 --- a/datahub-graphql-core/src/main/resources/search.graphql +++ b/datahub-graphql-core/src/main/resources/search.graphql @@ -405,6 +405,11 @@ type AutoCompleteResultForEntity { The autocompletion results for specified entity type """ suggestions: [String!]! + + """ + A list of entities to render in autocomplete + """ + entities: [Entity!]! } """ @@ -435,6 +440,11 @@ type AutoCompleteResults { The autocompletion results """ suggestions: [String!]! + + """ + A list of entities to render in autocomplete + """ + entities: [Entity!]! } """ diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index 2950928cc829b..6878271166d2d 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -282,6 +282,7 @@ const dataset2 = { downstream: null, health: null, assertions: null, + status: null, }; export const dataset3 = { @@ -484,6 +485,7 @@ export const dataset3 = { relationships: null, health: null, assertions: null, + status: null, } as Dataset; export const dataset4 = { @@ -1082,6 +1084,7 @@ export const dataJob1 = { ], }, domain: null, + status: null, } as DataJob; export const dataJob2 = { @@ -1204,6 +1207,7 @@ export const dataJob3 = { domain: null, upstream: null, downstream: null, + status: null, } as DataJob; export const mlModel = { @@ -1278,6 +1282,7 @@ export const mlModel = { outgoing: null, upstream: null, downstream: null, + status: null, } as MlModel; export const mlModelGroup = { @@ -1340,6 +1345,7 @@ export const mlModelGroup = { outgoing: null, upstream: null, downstream: null, + status: null, } as MlModelGroup; export const recommendationModules = [ @@ -1594,7 +1600,8 @@ export const mocks = [ suggestions: [ { type: EntityType.Dataset, - suggestions: ['The Great Test Dataset', 'Some other test'], + suggestions: ['The Great Test Dataset', 'Some Other Dataset'], + entities: [dataset1, dataset2], }, ], }, @@ -1618,7 +1625,8 @@ export const mocks = [ suggestions: [ { type: EntityType.Dataset, - suggestions: ['The Great Test Dataset', 'Some other test'], + suggestions: ['The Great Test Dataset', 'Some Other Dataset'], + entities: [dataset1, dataset2], }, ], }, @@ -1640,6 +1648,7 @@ export const mocks = [ autoComplete: { query: 'j', suggestions: ['jjoyce'], + entities: [user1], }, }, }, @@ -2967,4 +2976,69 @@ export const mocks = [ }, }, }, + { + request: { + query: GetSearchResultsForMultipleDocument, + variables: { + input: { + types: [], + query: '*', + start: 0, + count: 20, + filters: [], + }, + }, + }, + result: { + data: { + __typename: 'Query', + searchAcrossEntities: { + __typename: 'SearchResults', + start: 0, + count: 1, + total: 1, + searchResults: [ + { + entity: { + __typename: 'Dataset', + ...dataset3, + }, + matchedFields: [], + insights: [], + }, + { + entity: { + __typename: 'Dataset', + ...dataset4, + }, + matchedFields: [], + insights: [], + }, + ], + facets: [ + { + field: 'origin', + displayName: 'origin', + aggregations: [ + { + value: 'PROD', + count: 3, + entity: null, + }, + ], + }, + { + field: 'platform', + displayName: 'platform', + aggregations: [ + { value: 'hdfs', count: 1, entity: null }, + { value: 'mysql', count: 1, entity: null }, + { value: 'kafka', count: 1, entity: null }, + ], + }, + ], + }, + } as GetSearchResultsForMultipleQuery, + }, + }, ]; diff --git a/datahub-web-react/src/app/browse/BrowseResultsPage.tsx b/datahub-web-react/src/app/browse/BrowseResultsPage.tsx index 25e85f67b1825..485667b8f2634 100644 --- a/datahub-web-react/src/app/browse/BrowseResultsPage.tsx +++ b/datahub-web-react/src/app/browse/BrowseResultsPage.tsx @@ -61,7 +61,7 @@ export const BrowseResultsPage = () => { {loading && } - {data && data.browse && ( + {data && data.browse && !loading && ( (type: EntityType, data: T): FetchedEntity | undefined { const entity = validatedGet(type, this.entityTypeToEntity); - return entity.getLineageVizConfig?.(data) || undefined; + const genericEntityProperties = this.getGenericEntityProperties(type, data); + return ( + ({ + ...entity.getLineageVizConfig?.(data), + status: genericEntityProperties?.status, + } as FetchedEntity) || undefined + ); } getDisplayName(type: EntityType, data: T): string { diff --git a/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx b/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx index 9aadab5dd26e8..fb1135f6b3678 100644 --- a/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx +++ b/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx @@ -5,7 +5,7 @@ import { useGetDashboardQuery, useUpdateDashboardMutation, } from '../../../graphql/dashboard.generated'; -import { Dashboard, EntityType, PlatformType, SearchResult } from '../../../types.generated'; +import { Dashboard, EntityType, OwnershipType, PlatformType, SearchResult } from '../../../types.generated'; import { EntityAndType } from '../../lineage/types'; import { Entity, IconStyleType, PreviewType } from '../Entity'; import { EntityProfile } from '../shared/containers/profile/EntityProfile'; @@ -104,6 +104,9 @@ export class DashboardEntity implements Entity { }, { component: SidebarOwnerSection, + properties: { + defaultOwnerType: OwnershipType.TechnicalOwner, + }, }, { component: SidebarDomainSection, diff --git a/datahub-web-react/src/app/entity/dataFlow/DataFlowEntity.tsx b/datahub-web-react/src/app/entity/dataFlow/DataFlowEntity.tsx index e59856ea00abb..28832e25cf274 100644 --- a/datahub-web-react/src/app/entity/dataFlow/DataFlowEntity.tsx +++ b/datahub-web-react/src/app/entity/dataFlow/DataFlowEntity.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { ShareAltOutlined } from '@ant-design/icons'; -import { DataFlow, EntityType, PlatformType, SearchResult } from '../../../types.generated'; +import { DataFlow, EntityType, OwnershipType, PlatformType, SearchResult } from '../../../types.generated'; import { Preview } from './preview/Preview'; import { Entity, IconStyleType, PreviewType } from '../Entity'; import { EntityProfile } from '../shared/containers/profile/EntityProfile'; @@ -89,6 +89,9 @@ export class DataFlowEntity implements Entity { }, { component: SidebarOwnerSection, + properties: { + defaultOwnerType: OwnershipType.TechnicalOwner, + }, }, { component: SidebarDomainSection, diff --git a/datahub-web-react/src/app/entity/dataJob/DataJobEntity.tsx b/datahub-web-react/src/app/entity/dataJob/DataJobEntity.tsx index 3b261446af898..2d66322c807cd 100644 --- a/datahub-web-react/src/app/entity/dataJob/DataJobEntity.tsx +++ b/datahub-web-react/src/app/entity/dataJob/DataJobEntity.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { ConsoleSqlOutlined } from '@ant-design/icons'; -import { DataJob, EntityType, PlatformType, SearchResult } from '../../../types.generated'; +import { DataJob, EntityType, OwnershipType, PlatformType, SearchResult } from '../../../types.generated'; import { Preview } from './preview/Preview'; import { Entity, IconStyleType, PreviewType } from '../Entity'; import { EntityProfile } from '../shared/containers/profile/EntityProfile'; @@ -101,6 +101,9 @@ export class DataJobEntity implements Entity { }, { component: SidebarOwnerSection, + properties: { + defaultOwnerType: OwnershipType.TechnicalOwner, + }, }, { component: SidebarDomainSection, diff --git a/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx b/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx index f952c15b891f7..b0b67f3ca6247 100644 --- a/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx +++ b/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { DatabaseFilled, DatabaseOutlined } from '@ant-design/icons'; import { Typography } from 'antd'; -import { Dataset, EntityType, SearchResult } from '../../../types.generated'; +import { Dataset, EntityType, OwnershipType, SearchResult } from '../../../types.generated'; import { Entity, IconStyleType, PreviewType } from '../Entity'; import { Preview } from './preview/Preview'; import { FIELDS_TO_HIGHLIGHT } from './search/highlights'; @@ -111,9 +111,13 @@ export class DatasetEntity implements Entity { component: LineageTab, display: { visible: (_, _1) => true, - enabled: (_, dataset: GetDatasetQuery) => - (dataset?.dataset?.upstream?.count || 0) > 0 || - (dataset?.dataset?.downstream?.count || 0) > 0, + enabled: (_, dataset: GetDatasetQuery) => { + console.log(dataset?.dataset?.upstream, dataset?.dataset?.downstream); + return ( + (dataset?.dataset?.upstream?.total || 0) > 0 || + (dataset?.dataset?.downstream?.total || 0) > 0 + ); + }, }, }, { @@ -175,6 +179,9 @@ export class DatasetEntity implements Entity { }, { component: SidebarOwnerSection, + properties: { + defaultOwnerType: OwnershipType.TechnicalOwner, + }, }, { component: SidebarDomainSection, diff --git a/datahub-web-react/src/app/entity/domain/DomainEntity.tsx b/datahub-web-react/src/app/entity/domain/DomainEntity.tsx index 92af08fb73cc7..edf6a4367a32d 100644 --- a/datahub-web-react/src/app/entity/domain/DomainEntity.tsx +++ b/datahub-web-react/src/app/entity/domain/DomainEntity.tsx @@ -79,6 +79,9 @@ export class DomainEntity implements Entity { }, { component: SidebarOwnerSection, + properties: { + hideOwnerType: true, + }, }, ]} /> diff --git a/datahub-web-react/src/app/entity/group/GroupOwnerSideBarSection.tsx b/datahub-web-react/src/app/entity/group/GroupOwnerSideBarSection.tsx index 6fad53f42fc81..d637bcc457f7f 100644 --- a/datahub-web-react/src/app/entity/group/GroupOwnerSideBarSection.tsx +++ b/datahub-web-react/src/app/entity/group/GroupOwnerSideBarSection.tsx @@ -52,6 +52,7 @@ export default function GroupOwnerSideBarSection({ urn, ownership, refetch }: Pr Promise; }; @@ -24,7 +26,7 @@ const OwnerTag = styled(Tag)` align-items: center; `; -export const ExpandedOwner = ({ entityUrn, owner, refetch }: Props) => { +export const ExpandedOwner = ({ entityUrn, owner, hidePopOver, refetch }: Props) => { const entityRegistry = useEntityRegistry(); const { entityType } = useEntityData(); const [removeOwnerMutation] = useRemoveOwnerMutation(); @@ -85,9 +87,18 @@ export const ExpandedOwner = ({ entityUrn, owner, refetch }: Props) => { - - {name} - + {(hidePopOver && <>{name}) || ( + {getNameFromType(owner.type)}} + content={ + {getDescriptionFromType(owner.type)} + } + > + {name} + + )} ); 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 7fd0c888b8d52..91d1498b883da 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 @@ -231,6 +231,12 @@ export const EntityProfile = ({ > <> {showBrowseBar && } + {entityData?.status?.removed === true && ( + + )} {loading && } {!loading && error && ( diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Ownership/AddOwnerModal.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Ownership/AddOwnerModal.tsx index cae970b37b05a..b0003a642a6d5 100644 --- a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Ownership/AddOwnerModal.tsx +++ b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/Ownership/AddOwnerModal.tsx @@ -4,11 +4,17 @@ import styled from 'styled-components'; import { Link } from 'react-router-dom'; import { useAddOwnerMutation } from '../../../../../../../graphql/mutations.generated'; import { useGetSearchResultsLazyQuery } from '../../../../../../../graphql/search.generated'; -import { CorpUser, EntityType, OwnerEntityType, SearchResult } from '../../../../../../../types.generated'; +import { + CorpUser, + EntityType, + OwnerEntityType, + OwnershipType, + SearchResult, +} from '../../../../../../../types.generated'; import { useEntityRegistry } from '../../../../../../useEntityRegistry'; import { CustomAvatar } from '../../../../../../shared/avatar'; import analytics, { EventType, EntityActionType } from '../../../../../../analytics'; -import { useEnterKeyListener } from '../../../../../../shared/useEnterKeyListener'; +import { OWNERSHIP_DISPLAY_TYPES } from './ownershipUtils'; const SearchResultContainer = styled.div` display: flex; @@ -31,6 +37,8 @@ type Props = { urn: string; type: EntityType; visible: boolean; + defaultOwnerType?: OwnershipType; + hideOwnerType?: boolean | undefined; onClose: () => void; refetch?: () => Promise; }; @@ -41,9 +49,10 @@ type SelectedActor = { urn: string; }; -export const AddOwnerModal = ({ urn, type, visible, onClose, refetch }: Props) => { +export const AddOwnerModal = ({ urn, type, visible, hideOwnerType, defaultOwnerType, onClose, refetch }: Props) => { const entityRegistry = useEntityRegistry(); const [selectedActor, setSelectedActor] = useState(undefined); + const [selectedOwnerType, setSelectedOwnerType] = useState(defaultOwnerType || OwnershipType.None); const [userSearch, { data: userSearchData }] = useGetSearchResultsLazyQuery(); const [groupSearch, { data: groupSearchData }] = useGetSearchResultsLazyQuery(); const [addOwnerMutation] = useAddOwnerMutation(); @@ -66,6 +75,7 @@ export const AddOwnerModal = ({ urn, type, visible, onClose, refetch }: Props) = variables: { input: { ownerUrn: selectedActor.urn, + type: selectedOwnerType, resourceUrn: urn, ownerEntityType, }, @@ -112,6 +122,11 @@ export const AddOwnerModal = ({ urn, type, visible, onClose, refetch }: Props) = setSelectedActor(undefined); }; + // When a user search result is selected, set the urn as the selected urn. + const onSelectOwnerType = (newType: OwnershipType) => { + setSelectedOwnerType(newType); + }; + // Invokes the search API as the user types const handleSearch = (entityType: EntityType, text: string, searchQuery: any) => { if (text.length > 2) { @@ -172,16 +187,20 @@ export const AddOwnerModal = ({ urn, type, visible, onClose, refetch }: Props) = }; const selectValue = (selectedActor && [selectedActor.displayName]) || []; + const ownershipTypes = OWNERSHIP_DISPLAY_TYPES; // Handle the Enter press - useEnterKeyListener({ - querySelectorToExecuteClick: '#addOwnerButton', - }); + // TODO: Allow user to be selected prior to executed the save. + // useEnterKeyListener({ + // querySelectorToExecuteClick: selectedActor && '#addOwnerButton', + // }); + return (