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 d1da55268a50d..079a20619d1ea 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 @@ -67,6 +67,7 @@ import com.linkedin.datahub.graphql.generated.EntityPath; import com.linkedin.datahub.graphql.generated.EntityRelationship; import com.linkedin.datahub.graphql.generated.EntityRelationshipLegacy; +import com.linkedin.datahub.graphql.generated.FacetMetadata; import com.linkedin.datahub.graphql.generated.ForeignKeyConstraint; import com.linkedin.datahub.graphql.generated.FormActorAssignment; import com.linkedin.datahub.graphql.generated.FreshnessContract; @@ -1474,6 +1475,19 @@ private void configureGenericEntityResolvers(final RuntimeWiring.Builder builder "entity", new EntityTypeResolver( entityTypes, (env) -> ((BrowsePathEntry) env.getSource()).getEntity()))) + .type( + "FacetMetadata", + typeWiring -> + typeWiring.dataFetcher( + "entity", + new EntityTypeResolver( + entityTypes, + (env) -> { + FacetMetadata facetMetadata = env.getSource(); + return facetMetadata.getEntity() != null + ? facetMetadata.getEntity() + : null; + }))) .type( "LineageRelationship", typeWiring -> diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/authorization/AuthorizationUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/authorization/AuthorizationUtils.java index ca60acaa80538..c25d6af75fe76 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/authorization/AuthorizationUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/authorization/AuthorizationUtils.java @@ -339,6 +339,11 @@ public static boolean canManageStructuredProperties(@Nonnull QueryContext contex context.getOperationContext(), PoliciesConfig.MANAGE_STRUCTURED_PROPERTIES_PRIVILEGE); } + public static boolean canViewStructuredPropertiesPage(@Nonnull QueryContext context) { + return AuthUtil.isAuthorized( + context.getOperationContext(), PoliciesConfig.VIEW_STRUCTURED_PROPERTIES_PAGE_PRIVILEGE); + } + public static boolean canManageForms(@Nonnull QueryContext context) { return AuthUtil.isAuthorized( context.getOperationContext(), PoliciesConfig.MANAGE_DOCUMENTATION_FORMS_PRIVILEGE); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java index b1101ae3ee865..8297392e642d5 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/MeResolver.java @@ -93,6 +93,10 @@ public CompletableFuture get(DataFetchingEnvironment environm BusinessAttributeAuthorizationUtils.canCreateBusinessAttribute(context)); platformPrivileges.setManageBusinessAttributes( BusinessAttributeAuthorizationUtils.canManageBusinessAttribute(context)); + platformPrivileges.setManageStructuredProperties( + AuthorizationUtils.canManageStructuredProperties(context)); + platformPrivileges.setViewStructuredPropertiesPage( + AuthorizationUtils.canViewStructuredPropertiesPage(context)); // Construct and return authenticated user object. final AuthenticatedUser authUser = new AuthenticatedUser(); authUser.setCorpUser(corpUser); 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 259d05c631557..3647eb55b2583 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 @@ -188,6 +188,7 @@ public CompletableFuture get(final DataFetchingEnvironment environmen .setDataContractsEnabled(_featureFlags.isDataContractsEnabled()) .setEditableDatasetNameEnabled(_featureFlags.isEditableDatasetNameEnabled()) .setShowSeparateSiblings(_featureFlags.isShowSeparateSiblings()) + .setShowManageStructuredProperties(_featureFlags.isShowManageStructuredProperties()) .build(); appConfig.setFeatureFlags(featureFlagsConfig); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/AggregateAcrossEntitiesResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/AggregateAcrossEntitiesResolver.java index 29b71d95ad974..31ed2de7a6d51 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/AggregateAcrossEntitiesResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/AggregateAcrossEntitiesResolver.java @@ -66,11 +66,17 @@ public CompletableFuture get(DataFetchingEnvironment environme final Filter inputFilter = ResolverUtils.buildFilter(null, input.getOrFilters()); - final SearchFlags searchFlags = mapInputFlags(context, input.getSearchFlags()); + final SearchFlags searchFlags = + input.getSearchFlags() != null + ? mapInputFlags(context, input.getSearchFlags()) + : new SearchFlags(); final List facets = input.getFacets() != null && input.getFacets().size() > 0 ? input.getFacets() : null; + // do not include default facets if we're requesting any facets specifically + searchFlags.setIncludeDefaultFacets(facets == null || facets.size() <= 0); + List finalEntities = maybeResolvedView != null ? SearchUtils.intersectEntityTypes( diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchAcrossEntitiesResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchAcrossEntitiesResolver.java index d103704146d39..29bc3a82a1649 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchAcrossEntitiesResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/search/SearchAcrossEntitiesResolver.java @@ -2,19 +2,28 @@ import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument; import static com.linkedin.datahub.graphql.resolvers.search.SearchUtils.*; +import static com.linkedin.datahub.graphql.resolvers.search.SearchUtils.getEntityNames; +import com.google.common.collect.ImmutableList; import com.linkedin.common.urn.UrnUtils; import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils; +import com.linkedin.datahub.graphql.generated.EntityType; import com.linkedin.datahub.graphql.generated.SearchAcrossEntitiesInput; import com.linkedin.datahub.graphql.generated.SearchResults; import com.linkedin.datahub.graphql.resolvers.ResolverUtils; import com.linkedin.datahub.graphql.types.mappers.UrnSearchResultsMapper; import com.linkedin.entity.client.EntityClient; import com.linkedin.metadata.query.SearchFlags; +import com.linkedin.metadata.query.filter.Condition; +import com.linkedin.metadata.query.filter.ConjunctiveCriterion; +import com.linkedin.metadata.query.filter.ConjunctiveCriterionArray; +import com.linkedin.metadata.query.filter.CriterionArray; import com.linkedin.metadata.query.filter.Filter; import com.linkedin.metadata.query.filter.SortCriterion; +import com.linkedin.metadata.search.SearchResult; import com.linkedin.metadata.service.ViewService; +import com.linkedin.metadata.utils.CriterionUtils; import com.linkedin.view.DataHubViewInfo; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; @@ -64,24 +73,7 @@ public CompletableFuture get(DataFetchingEnvironment environment) ResolverUtils.buildFilter(input.getFilters(), input.getOrFilters()); SearchFlags searchFlags = mapInputFlags(context, input.getSearchFlags()); - List sortCriteria; - if (input.getSortInput() != null) { - if (input.getSortInput().getSortCriteria() != null) { - sortCriteria = - input.getSortInput().getSortCriteria().stream() - .map(SearchUtils::mapSortCriterion) - .collect(Collectors.toList()); - } else { - sortCriteria = - input.getSortInput().getSortCriterion() != null - ? Collections.singletonList( - mapSortCriterion(input.getSortInput().getSortCriterion())) - : Collections.emptyList(); - } - - } else { - sortCriteria = Collections.emptyList(); - } + List sortCriteria = SearchUtils.getSortCriteria(input.getSortInput()); try { log.debug( @@ -101,6 +93,14 @@ public CompletableFuture get(DataFetchingEnvironment environment) return SearchUtils.createEmptySearchResults(start, count); } + boolean shouldIncludeStructuredPropertyFacets = + input.getSearchFlags() != null + && input.getSearchFlags().getIncludeStructuredPropertyFacets() != null + ? input.getSearchFlags().getIncludeStructuredPropertyFacets() + : false; + List structuredPropertyFacets = + shouldIncludeStructuredPropertyFacets ? getStructuredPropertyFacets(context) : null; + return UrnSearchResultsMapper.map( context, _entityClient.searchAcrossEntities( @@ -113,7 +113,8 @@ public CompletableFuture get(DataFetchingEnvironment environment) : baseFilter, start, count, - sortCriteria)); + sortCriteria, + structuredPropertyFacets)); } catch (Exception e) { log.error( "Failed to execute search for multiple entities: entity types {}, query {}, filters: {}, start: {}, count: {}", @@ -133,4 +134,45 @@ public CompletableFuture get(DataFetchingEnvironment environment) this.getClass().getSimpleName(), "get"); } + + private List getStructuredPropertyFacets(final QueryContext context) { + try { + SearchFlags searchFlags = new SearchFlags().setSkipCache(true); + SearchResult result = + _entityClient.searchAcrossEntities( + context.getOperationContext().withSearchFlags(flags -> searchFlags), + getEntityNames(ImmutableList.of(EntityType.STRUCTURED_PROPERTY)), + "*", + createStructuredPropertyFilter(), + 0, + 100, + Collections.emptyList(), + null); + return result.getEntities().stream() + .map(entity -> String.format("structuredProperties.%s", entity.getEntity().getId())) + .collect(Collectors.toList()); + } catch (Exception e) { + log.error("Failed to get structured property facets to filter on", e); + return Collections.emptyList(); + } + } + + private Filter createStructuredPropertyFilter() { + return new Filter() + .setOr( + new ConjunctiveCriterionArray( + ImmutableList.of( + new ConjunctiveCriterion() + .setAnd( + new CriterionArray( + ImmutableList.of( + CriterionUtils.buildCriterion( + "filterStatus", Condition.EQUAL, "ENABLED")))), + new ConjunctiveCriterion() + .setAnd( + new CriterionArray( + ImmutableList.of( + CriterionUtils.buildCriterion( + "showInSearchFilters", Condition.EQUAL, "true"))))))); + } } 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 04777c3fcdb4e..a01b3aaec9c98 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 @@ -22,6 +22,7 @@ import com.linkedin.datahub.graphql.generated.EntityType; import com.linkedin.datahub.graphql.generated.FacetFilterInput; import com.linkedin.datahub.graphql.generated.SearchResults; +import com.linkedin.datahub.graphql.generated.SearchSortInput; import com.linkedin.datahub.graphql.types.common.mappers.SearchFlagsInputMapper; import com.linkedin.datahub.graphql.types.entitytype.EntityTypeMapper; import com.linkedin.metadata.query.SearchFlags; @@ -326,4 +327,25 @@ public static SearchResults createEmptySearchResults(final int start, final int result.setFacets(new ArrayList<>()); return result; } + + public static List getSortCriteria(@Nullable final SearchSortInput sortInput) { + List sortCriteria; + if (sortInput != null) { + if (sortInput.getSortCriteria() != null) { + sortCriteria = + sortInput.getSortCriteria().stream() + .map(SearchUtils::mapSortCriterion) + .collect(Collectors.toList()); + } else { + sortCriteria = + sortInput.getSortCriterion() != null + ? Collections.singletonList(mapSortCriterion(sortInput.getSortCriterion())) + : new ArrayList<>(); + } + } else { + sortCriteria = new ArrayList<>(); + } + + return sortCriteria; + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/MapperUtils.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/MapperUtils.java index 0d69e62c621a6..8fe58df2d2ede 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/MapperUtils.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/mappers/MapperUtils.java @@ -70,6 +70,9 @@ public static FacetMetadata mapFacet( aggregationFacets.stream() .map(facet -> facet.equals("entity") || facet.contains("_entityType")) .collect(Collectors.toList()); + if (aggregationMetadata.getEntity() != null) { + facetMetadata.setEntity(UrnToEntityMapper.map(context, aggregationMetadata.getEntity())); + } facetMetadata.setField(aggregationMetadata.getName()); facetMetadata.setDisplayName( Optional.ofNullable(aggregationMetadata.getDisplayName()) diff --git a/datahub-graphql-core/src/main/resources/app.graphql b/datahub-graphql-core/src/main/resources/app.graphql index 262d2384d84ad..2868890368723 100644 --- a/datahub-graphql-core/src/main/resources/app.graphql +++ b/datahub-graphql-core/src/main/resources/app.graphql @@ -156,6 +156,15 @@ type PlatformPrivileges { """ manageBusinessAttributes: Boolean! + """ + Whether the user can create, edit, and delete structured properties. + """ + manageStructuredProperties: Boolean! + + """ + Whether the user can view the manage structured properties page. + """ + viewStructuredPropertiesPage: Boolean! } """ @@ -517,6 +526,11 @@ type FeatureFlagsConfig { If turned on, all siblings will be separated with no way to get to a "combined" sibling view """ showSeparateSiblings: Boolean! + + """ + If turned on, show the manage structured properties tab in the govern dropdown + """ + showManageStructuredProperties: Boolean! } """ diff --git a/datahub-graphql-core/src/main/resources/search.graphql b/datahub-graphql-core/src/main/resources/search.graphql index d0f669f05f959..82bfb9ee26fc4 100644 --- a/datahub-graphql-core/src/main/resources/search.graphql +++ b/datahub-graphql-core/src/main/resources/search.graphql @@ -167,6 +167,11 @@ input SearchFlags { fields to include for custom Highlighting """ customHighlightingFields: [String!] + + """ + Whether or not to fetch and request for structured property facets when doing a search + """ + includeStructuredPropertyFacets: Boolean } """ @@ -872,6 +877,11 @@ type FacetMetadata { """ displayName: String + """ + Entity corresponding to the facet + """ + entity: Entity + """ Aggregated search result counts by value of the field """ diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/search/SearchAcrossEntitiesResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/search/SearchAcrossEntitiesResolverTest.java index 42768b8a2de21..89d218683e33e 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/search/SearchAcrossEntitiesResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/search/SearchAcrossEntitiesResolverTest.java @@ -471,7 +471,8 @@ private static EntityClient initMockEntityClient( Mockito.eq(filter), Mockito.eq(start), Mockito.eq(limit), - Mockito.eq(Collections.emptyList()))) + Mockito.eq(Collections.emptyList()), + Mockito.eq(null))) .thenReturn(result); return client; } @@ -496,7 +497,8 @@ private static void verifyMockEntityClient( Mockito.eq(filter), Mockito.eq(start), Mockito.eq(limit), - Mockito.eq(Collections.emptyList())); + Mockito.eq(Collections.emptyList()), + Mockito.eq(null)); } private static void verifyMockViewService(ViewService mockService, Urn viewUrn) { diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index 329d6250e576a..73a789030ce6f 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -2204,7 +2204,7 @@ export const mocks = [ count: 10, filters: [], orFilters: [], - searchFlags: { getSuggestions: true }, + searchFlags: { getSuggestions: true, includeStructuredPropertyFacets: true }, }, }, }, @@ -2244,6 +2244,7 @@ export const mocks = [ field: 'origin', displayName: 'origin', aggregations: [{ value: 'PROD', count: 3, entity: null }], + entity: null, }, { field: '_entityType', @@ -2252,6 +2253,7 @@ export const mocks = [ { count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' }, { count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' }, ], + entity: null, }, { field: 'platform', @@ -2261,6 +2263,7 @@ export const mocks = [ { value: 'MySQL', count: 1, entity: null }, { value: 'Kafka', count: 1, entity: null }, ], + entity: null, }, ], suggestions: [], @@ -2290,7 +2293,7 @@ export const mocks = [ ], }, ], - searchFlags: { getSuggestions: true }, + searchFlags: { getSuggestions: true, includeStructuredPropertyFacets: true }, }, }, }, @@ -2325,6 +2328,7 @@ export const mocks = [ entity: null, }, ], + entity: null, }, { field: '_entityType', @@ -2333,6 +2337,7 @@ export const mocks = [ { count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' }, { count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' }, ], + entity: null, }, { __typename: 'FacetMetadata', @@ -2343,6 +2348,7 @@ export const mocks = [ { value: 'mysql', count: 1, entity: null }, { value: 'kafka', count: 1, entity: null }, ], + entity: null, }, ], suggestions: [], @@ -2393,6 +2399,7 @@ export const mocks = [ entity: null, }, ], + entity: null, }, { field: '_entityType', @@ -2401,6 +2408,7 @@ export const mocks = [ { count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' }, { count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' }, ], + entity: null, }, { field: 'platform', @@ -2410,6 +2418,7 @@ export const mocks = [ { value: 'mysql', count: 1, entity: null }, { value: 'kafka', count: 1, entity: null }, ], + entity: null, }, ], }, @@ -2464,7 +2473,7 @@ export const mocks = [ ], }, ], - searchFlags: { getSuggestions: true }, + searchFlags: { getSuggestions: true, includeStructuredPropertyFacets: true }, }, }, }, @@ -2501,6 +2510,7 @@ export const mocks = [ entity: null, }, ], + entity: null, }, { __typename: 'FacetMetadata', @@ -2510,6 +2520,7 @@ export const mocks = [ { count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' }, { count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' }, ], + entity: null, }, { __typename: 'FacetMetadata', @@ -2520,6 +2531,7 @@ export const mocks = [ { value: 'mysql', count: 1, entity: null, __typename: 'AggregationMetadata' }, { value: 'kafka', count: 1, entity: null, __typename: 'AggregationMetadata' }, ], + entity: null, }, ], }, @@ -2669,6 +2681,7 @@ export const mocks = [ entity: null, }, ], + entity: null, }, { field: '_entityType', @@ -2677,6 +2690,7 @@ export const mocks = [ { count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' }, { count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' }, ], + entity: null, }, { field: 'platform', @@ -2686,6 +2700,7 @@ export const mocks = [ { value: 'mysql', count: 1, entity: null }, { value: 'kafka', count: 1, entity: null }, ], + entity: null, }, ], }, @@ -2743,6 +2758,7 @@ export const mocks = [ entity: null, }, ], + entity: null, }, { field: '_entityType', @@ -2751,6 +2767,7 @@ export const mocks = [ { count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' }, { count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' }, ], + entity: null, }, { field: 'platform', @@ -2760,6 +2777,7 @@ export const mocks = [ { value: 'mysql', count: 1, entity: null }, { value: 'kafka', count: 1, entity: null }, ], + entity: null, }, ], }, @@ -2809,6 +2827,7 @@ export const mocks = [ entity: null, }, ], + entity: null, }, { field: 'platform', @@ -2822,6 +2841,7 @@ export const mocks = [ { value: 'mysql', count: 1, entity: null }, { value: 'kafka', count: 1, entity: null }, ], + entity: null, }, ], }, @@ -2953,6 +2973,7 @@ export const mocks = [ entity: null, }, ], + entity: null, }, { field: 'platform', @@ -2966,6 +2987,7 @@ export const mocks = [ { value: 'mysql', count: 1, entity: null }, { value: 'kafka', count: 1, entity: null }, ], + entity: null, }, ], }, @@ -3013,7 +3035,7 @@ export const mocks = [ ], }, ], - searchFlags: { getSuggestions: true }, + searchFlags: { getSuggestions: true, includeStructuredPropertyFacets: true }, }, }, }, @@ -3050,6 +3072,7 @@ export const mocks = [ entity: null, }, ], + entity: null, }, // { // displayName: 'Domain', @@ -3071,6 +3094,7 @@ export const mocks = [ { count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' }, { count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' }, ], + entity: null, }, { __typename: 'FacetMetadata', @@ -3096,6 +3120,7 @@ export const mocks = [ entity: null, }, ], + entity: null, }, ], }, @@ -3181,7 +3206,7 @@ export const mocks = [ ], }, ], - searchFlags: { getSuggestions: true }, + searchFlags: { getSuggestions: true, includeStructuredPropertyFacets: true }, }, }, }, @@ -3215,6 +3240,7 @@ export const mocks = [ entity: null, }, ], + entity: null, }, { field: 'platform', @@ -3228,6 +3254,7 @@ export const mocks = [ { value: 'mysql', count: 1, entity: null }, { value: 'kafka', count: 1, entity: null }, ], + entity: null, }, ], }, @@ -3256,7 +3283,7 @@ export const mocks = [ ], }, ], - searchFlags: { getSuggestions: true }, + searchFlags: { getSuggestions: true, includeStructuredPropertyFacets: true }, }, }, }, @@ -3290,6 +3317,7 @@ export const mocks = [ entity: null, }, ], + entity: null, }, { field: '_entityType', @@ -3298,6 +3326,7 @@ export const mocks = [ { count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' }, { count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' }, ], + entity: null, }, { field: 'platform', @@ -3307,6 +3336,7 @@ export const mocks = [ { value: 'mysql', count: 1, entity: null }, { value: 'kafka', count: 1, entity: null }, ], + entity: null, }, ], }, @@ -3335,7 +3365,7 @@ export const mocks = [ ], }, ], - searchFlags: { getSuggestions: true }, + searchFlags: { getSuggestions: true, includeStructuredPropertyFacets: true }, }, }, }, @@ -3377,6 +3407,7 @@ export const mocks = [ entity: null, }, ], + entity: null, }, { field: '_entityType', @@ -3385,6 +3416,7 @@ export const mocks = [ { count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' }, { count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' }, ], + entity: null, }, { field: 'platform', @@ -3394,6 +3426,7 @@ export const mocks = [ { value: 'mysql', count: 1, entity: null }, { value: 'kafka', count: 1, entity: null }, ], + entity: null, }, ], }, @@ -3428,7 +3461,7 @@ export const mocks = [ ], }, ], - searchFlags: { getSuggestions: true }, + searchFlags: { getSuggestions: true, includeStructuredPropertyFacets: true }, }, }, }, @@ -3465,6 +3498,7 @@ export const mocks = [ entity: null, }, ], + entity: null, }, { __typename: 'FacetMetadata', @@ -3474,6 +3508,7 @@ export const mocks = [ { count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' }, { count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' }, ], + entity: null, }, { __typename: 'FacetMetadata', @@ -3484,6 +3519,7 @@ export const mocks = [ { value: 'mysql', count: 1, entity: null, __typename: 'AggregationMetadata' }, { value: 'kafka', count: 1, entity: null, __typename: 'AggregationMetadata' }, ], + entity: null, }, ], }, @@ -3518,7 +3554,7 @@ export const mocks = [ ], }, ], - searchFlags: { getSuggestions: true }, + searchFlags: { getSuggestions: true, includeStructuredPropertyFacets: true }, }, }, }, @@ -3555,6 +3591,7 @@ export const mocks = [ __typename: 'AggregationMetadata', }, ], + entity: null, }, { __typename: 'FacetMetadata', @@ -3564,6 +3601,7 @@ export const mocks = [ { count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' }, { count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' }, ], + entity: null, }, { __typename: 'FacetMetadata', @@ -3574,6 +3612,7 @@ export const mocks = [ { value: 'mysql', count: 1, entity: null, __typename: 'AggregationMetadata' }, { value: 'kafka', count: 1, entity: null, __typename: 'AggregationMetadata' }, ], + entity: null, }, ], }, @@ -3635,6 +3674,8 @@ export const mocks = [ manageGlobalAnnouncements: true, createBusinessAttributes: true, manageBusinessAttributes: true, + manageStructuredProperties: true, + viewStructuredPropertiesPage: true, }, }, }, @@ -3722,7 +3763,7 @@ export const mocks = [ count: 10, filters: [], orFilters: [], - searchFlags: { getSuggestions: true }, + searchFlags: { getSuggestions: true, includeStructuredPropertyFacets: true }, }, }, }, @@ -3821,6 +3862,7 @@ export const mocks = [ entity: null, }, ], + entity: null, }, { field: '_entityType', @@ -3829,6 +3871,7 @@ export const mocks = [ { count: 37, entity: null, value: 'DATASET', __typename: 'AggregationMetadata' }, { count: 7, entity: null, value: 'CHART', __typename: 'AggregationMetadata' }, ], + entity: null, }, { field: 'platform', @@ -3838,6 +3881,7 @@ export const mocks = [ { value: 'mysql', count: 1, entity: null }, { value: 'kafka', count: 1, entity: null }, ], + entity: null, }, ], }, @@ -3912,4 +3956,6 @@ export const platformPrivileges: PlatformPrivileges = { manageGlobalAnnouncements: true, createBusinessAttributes: true, manageBusinessAttributes: true, + manageStructuredProperties: true, + viewStructuredPropertiesPage: true, }; diff --git a/datahub-web-react/src/alchemy-components/components/SearchBar/SearchBar.stories.tsx b/datahub-web-react/src/alchemy-components/components/SearchBar/SearchBar.stories.tsx new file mode 100644 index 0000000000000..9ae34356a71d6 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/SearchBar/SearchBar.stories.tsx @@ -0,0 +1,99 @@ +import React, { useState } from 'react'; +import { BADGE } from '@geometricpanda/storybook-addon-badges'; +import type { Meta, StoryObj } from '@storybook/react'; +import { GridList } from '@components/.docs/mdx-components'; +import { SearchBar, searchBarDefaults } from './SearchBar'; +import { SearchBarProps } from './types'; + +const meta = { + title: 'Components / Search Bar', + component: SearchBar, + + // Display Properties + parameters: { + layout: 'centered', + badges: [BADGE.STABLE, 'readyForDesignReview'], + docs: { + subtitle: 'A component that is used to get search bar', + }, + }, + + // Component-level argTypes + argTypes: { + placeholder: { + description: 'Placeholder of search bar.', + table: { + defaultValue: { summary: searchBarDefaults.placeholder }, + }, + control: { + type: 'text', + }, + }, + value: { + description: 'Value of the search bar.', + table: { + defaultValue: { summary: searchBarDefaults.value }, + }, + control: false, + }, + width: { + description: 'Width of the search bar.', + table: { + defaultValue: { summary: searchBarDefaults.width }, + }, + control: { + type: 'text', + }, + }, + allowClear: { + description: 'Whether clear button should be present.', + table: { + defaultValue: { summary: searchBarDefaults.allowClear?.toString() }, + }, + control: { + type: 'boolean', + }, + }, + onChange: { + description: 'On change function for the search bar.', + }, + }, + + // Define defaults + args: { + placeholder: searchBarDefaults.placeholder, + value: searchBarDefaults.value, + allowClear: searchBarDefaults.allowClear, + width: searchBarDefaults.width, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const SandboxWrapper = (props: SearchBarProps) => { + const [value, setValue] = useState(''); + + const handleChange = (newValue: string) => { + setValue(newValue); + }; + + return ; +}; + +export const sandbox: Story = { + tags: ['dev'], + render: (props) => { + return ; + }, +}; + +export const customWidths = () => ( + + + + + + +); diff --git a/datahub-web-react/src/alchemy-components/components/SearchBar/SearchBar.tsx b/datahub-web-react/src/alchemy-components/components/SearchBar/SearchBar.tsx new file mode 100644 index 0000000000000..f39f761058d8c --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/SearchBar/SearchBar.tsx @@ -0,0 +1,30 @@ +import { SearchOutlined } from '@ant-design/icons'; +import React from 'react'; +import { StyledSearchBar } from './components'; +import { SearchBarProps } from './types'; + +export const searchBarDefaults: SearchBarProps = { + placeholder: 'Search..', + value: '', + width: '272px', + allowClear: true, +}; + +export const SearchBar = ({ + placeholder = searchBarDefaults.placeholder, + value = searchBarDefaults.value, + width = searchBarDefaults.width, + allowClear = searchBarDefaults.allowClear, + onChange, +}: SearchBarProps) => { + return ( + onChange?.(e.target.value)} + value={value} + prefix={} + allowClear={allowClear} + $width={width} + /> + ); +}; diff --git a/datahub-web-react/src/alchemy-components/components/SearchBar/components.ts b/datahub-web-react/src/alchemy-components/components/SearchBar/components.ts new file mode 100644 index 0000000000000..7045801ddf092 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/SearchBar/components.ts @@ -0,0 +1,33 @@ +import { colors, typography } from '@src/alchemy-components/theme'; +import { Input } from 'antd'; +import styled from 'styled-components'; + +export const StyledSearchBar = styled(Input)<{ $width?: string }>` + height: 40px; + width: ${(props) => props.$width}; + display: flex; + align-items: center; + border-radius: 8px; + + input { + color: ${colors.gray[600]}; + font-size: ${typography.fontSizes.md} !important; + } + + .ant-input-prefix { + width: 20px; + color: ${colors.gray[1800]}; + + svg { + height: 16px; + width: 16px; + } + } + + &:hover, + &:focus, + &:focus-within { + border-color: ${colors.violet[300]} !important; + box-shadow: none !important; + } +`; diff --git a/datahub-web-react/src/alchemy-components/components/SearchBar/index.ts b/datahub-web-react/src/alchemy-components/components/SearchBar/index.ts new file mode 100644 index 0000000000000..8c1933163b29f --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/SearchBar/index.ts @@ -0,0 +1 @@ +export { SearchBar } from './SearchBar'; diff --git a/datahub-web-react/src/alchemy-components/components/SearchBar/types.ts b/datahub-web-react/src/alchemy-components/components/SearchBar/types.ts new file mode 100644 index 0000000000000..04ac218dc2cc6 --- /dev/null +++ b/datahub-web-react/src/alchemy-components/components/SearchBar/types.ts @@ -0,0 +1,7 @@ +export interface SearchBarProps { + placeholder?: string; + value?: string; + width?: string; + onChange?: (value: string) => void; + allowClear?: boolean; +} diff --git a/datahub-web-react/src/alchemy-components/index.ts b/datahub-web-react/src/alchemy-components/index.ts index 8ef4f73f4408f..7e40d343e884d 100644 --- a/datahub-web-react/src/alchemy-components/index.ts +++ b/datahub-web-react/src/alchemy-components/index.ts @@ -15,6 +15,7 @@ export * from './components/LineChart'; export * from './components/PageTitle'; export * from './components/Pills'; export * from './components/Popover'; +export * from './components/SearchBar'; export * from './components/Select'; export * from './components/Switch'; export * from './components/Table'; diff --git a/datahub-web-react/src/app/SearchRoutes.tsx b/datahub-web-react/src/app/SearchRoutes.tsx index 3343260c72bcf..024b6e0add15a 100644 --- a/datahub-web-react/src/app/SearchRoutes.tsx +++ b/datahub-web-react/src/app/SearchRoutes.tsx @@ -11,23 +11,36 @@ import { AnalyticsPage } from './analyticsDashboard/components/AnalyticsPage'; import { ManageIngestionPage } from './ingest/ManageIngestionPage'; import GlossaryRoutes from './glossary/GlossaryRoutes'; import { SettingsPage } from './settings/SettingsPage'; +import { useUserContext } from './context/useUserContext'; import DomainRoutes from './domain/DomainRoutes'; -import { useBusinessAttributesFlag, useIsAppConfigContextLoaded, useIsNestedDomainsEnabled } from './useAppConfig'; +import { + useAppConfig, + useBusinessAttributesFlag, + useIsAppConfigContextLoaded, + useIsNestedDomainsEnabled, +} from './useAppConfig'; import { ManageDomainsPage } from './domain/ManageDomainsPage'; import { BusinessAttributes } from './businessAttribute/BusinessAttributes'; +import StructuredProperties from './govern/structuredProperties/StructuredProperties'; /** * Container for all searchable page routes */ export const SearchRoutes = (): JSX.Element => { const entityRegistry = useEntityRegistry(); + const me = useUserContext(); const isNestedDomainsEnabled = useIsNestedDomainsEnabled(); const entities = isNestedDomainsEnabled ? entityRegistry.getEntitiesForSearchRoutes() : entityRegistry.getNonGlossaryEntities(); + const { config } = useAppConfig(); const businessAttributesFlag = useBusinessAttributesFlag(); const appConfigContextLoaded = useIsAppConfigContextLoaded(); + const showStructuredProperties = + config?.featureFlags?.showManageStructuredProperties && + (me.platformPrivileges?.manageStructuredProperties || me.platformPrivileges?.viewStructuredPropertiesPage); + return ( @@ -53,6 +66,9 @@ export const SearchRoutes = (): JSX.Element => { } /> } /> } /> + {showStructuredProperties && ( + } /> + )} { diff --git a/datahub-web-react/src/app/analytics/event.ts b/datahub-web-react/src/app/analytics/event.ts index c3a57830b8c50..9152f2fb8eedb 100644 --- a/datahub-web-react/src/app/analytics/event.ts +++ b/datahub-web-react/src/app/analytics/event.ts @@ -1,4 +1,12 @@ -import { DataHubViewType, EntityType, RecommendationRenderType, ScenarioType } from '../../types.generated'; +import { + AllowedValue, + DataHubViewType, + EntityType, + PropertyCardinality, + PropertyValueInput, + RecommendationRenderType, + ScenarioType, +} from '../../types.generated'; import { EmbedLookupNotFoundReason } from '../embed/lookup/constants'; import { Direction } from '../lineage/types'; import { FilterMode } from '../search/utils/constants'; @@ -82,6 +90,14 @@ export enum EventType { EmbedProfileViewInDataHubEvent, EmbedLookupNotFoundEvent, CreateBusinessAttributeEvent, + CreateStructuredPropertyClickEvent, + CreateStructuredPropertyEvent, + EditStructuredPropertyEvent, + DeleteStructuredPropertyEvent, + ViewStructuredPropertyEvent, + ApplyStructuredPropertyEvent, + UpdateStructuredPropertyOnAssetEvent, + RemoveStructuredPropertyEvent, } /** @@ -640,6 +656,64 @@ export interface CreateBusinessAttributeEvent extends BaseEvent { name: string; } +export interface CreateStructuredPropertyClickEvent extends BaseEvent { + type: EventType.CreateStructuredPropertyClickEvent; +} + +interface StructuredPropertyEvent extends BaseEvent { + propertyType: string; + appliesTo: string[]; + qualifiedName?: string; + allowedAssetTypes?: string[]; + allowedValues?: AllowedValue[]; + cardinality?: PropertyCardinality; + showInFilters?: boolean; + isHidden: boolean; + showInSearchFilters: boolean; + showAsAssetBadge: boolean; + showInAssetSummary: boolean; + showInColumnsTable: boolean; +} + +export interface CreateStructuredPropertyEvent extends StructuredPropertyEvent { + type: EventType.CreateStructuredPropertyEvent; +} + +export interface EditStructuredPropertyEvent extends StructuredPropertyEvent { + type: EventType.EditStructuredPropertyEvent; + propertyUrn: string; +} + +export interface DeleteStructuredPropertyEvent extends StructuredPropertyEvent { + type: EventType.DeleteStructuredPropertyEvent; + propertyUrn: string; +} + +export interface ViewStructuredPropertyEvent extends BaseEvent { + type: EventType.ViewStructuredPropertyEvent; + propertyUrn: string; +} + +interface StructuredPropertyOnAssetEvent extends BaseEvent { + propertyUrn: string; + propertyType: string; + assetUrn: string; + assetType: EntityType; +} +export interface ApplyStructuredPropertyEvent extends StructuredPropertyOnAssetEvent { + type: EventType.ApplyStructuredPropertyEvent; + values: PropertyValueInput[]; +} + +export interface UpdateStructuredPropertyOnAssetEvent extends StructuredPropertyOnAssetEvent { + type: EventType.UpdateStructuredPropertyOnAssetEvent; + values: PropertyValueInput[]; +} + +export interface RemoveStructuredPropertyEvent extends StructuredPropertyOnAssetEvent { + type: EventType.RemoveStructuredPropertyEvent; +} + /** * Event consisting of a union of specific event types. */ @@ -718,4 +792,12 @@ export type Event = | EmbedProfileViewEvent | EmbedProfileViewInDataHubEvent | EmbedLookupNotFoundEvent - | CreateBusinessAttributeEvent; + | CreateBusinessAttributeEvent + | CreateStructuredPropertyClickEvent + | CreateStructuredPropertyEvent + | EditStructuredPropertyEvent + | DeleteStructuredPropertyEvent + | ViewStructuredPropertyEvent + | ApplyStructuredPropertyEvent + | UpdateStructuredPropertyOnAssetEvent + | RemoveStructuredPropertyEvent; diff --git a/datahub-web-react/src/app/buildEntityRegistry.ts b/datahub-web-react/src/app/buildEntityRegistry.ts index 0b70986672be5..181ec7d328a58 100644 --- a/datahub-web-react/src/app/buildEntityRegistry.ts +++ b/datahub-web-react/src/app/buildEntityRegistry.ts @@ -24,6 +24,7 @@ import { RoleEntity } from './entity/Access/RoleEntity'; import { RestrictedEntity } from './entity/restricted/RestrictedEntity'; import { BusinessAttributeEntity } from './entity/businessAttribute/BusinessAttributeEntity'; import { SchemaFieldPropertiesEntity } from './entity/schemaField/SchemaFieldPropertiesEntity'; +import { StructuredPropertyEntity } from './entity/structuredProperty/StructuredPropertyEntity'; export default function buildEntityRegistry() { const registry = new EntityRegistry(); @@ -52,5 +53,6 @@ export default function buildEntityRegistry() { registry.register(new RestrictedEntity()); registry.register(new BusinessAttributeEntity()); registry.register(new SchemaFieldPropertiesEntity()); + registry.register(new StructuredPropertyEntity()); return registry; } diff --git a/datahub-web-react/src/app/entity/Access/RoleEntity.tsx b/datahub-web-react/src/app/entity/Access/RoleEntity.tsx index ab609b04f104a..58a1ba8dd793b 100644 --- a/datahub-web-react/src/app/entity/Access/RoleEntity.tsx +++ b/datahub-web-react/src/app/entity/Access/RoleEntity.tsx @@ -88,4 +88,8 @@ export class RoleEntity implements Entity { supportedCapabilities = () => { return new Set([EntityCapabilityType.OWNERS]); }; + + getGraphName = () => { + return 'roleEntity'; + }; } diff --git a/datahub-web-react/src/app/entity/Entity.tsx b/datahub-web-react/src/app/entity/Entity.tsx index 490f23330c594..c56c97454a1d5 100644 --- a/datahub-web-react/src/app/entity/Entity.tsx +++ b/datahub-web-react/src/app/entity/Entity.tsx @@ -172,6 +172,11 @@ export interface Entity { */ getGenericEntityProperties: (data: T) => GenericEntityProperties | null; + /** + * Returns the graph name of the entity, as it appears in the GMS entity registry + */ + getGraphName: () => string; + /** * Returns the supported features for the entity */ diff --git a/datahub-web-react/src/app/entity/EntityRegistry.tsx b/datahub-web-react/src/app/entity/EntityRegistry.tsx index 00e7385ff5784..0f65390f959df 100644 --- a/datahub-web-react/src/app/entity/EntityRegistry.tsx +++ b/datahub-web-react/src/app/entity/EntityRegistry.tsx @@ -241,4 +241,8 @@ export default class EntityRegistry { const entity = validatedGet(type, this.entityTypeToEntity); return entity.getCustomCardUrlPath?.(); } + + getGraphNameFromType(type: EntityType): string { + return validatedGet(type, this.entityTypeToEntity).getGraphName(); + } } diff --git a/datahub-web-react/src/app/entity/businessAttribute/BusinessAttributeEntity.tsx b/datahub-web-react/src/app/entity/businessAttribute/BusinessAttributeEntity.tsx index b827a3c37d6a5..442aaf735575a 100644 --- a/datahub-web-react/src/app/entity/businessAttribute/BusinessAttributeEntity.tsx +++ b/datahub-web-react/src/app/entity/businessAttribute/BusinessAttributeEntity.tsx @@ -59,6 +59,8 @@ export class BusinessAttributeEntity implements Entity { getCollectionName = () => 'Business Attributes'; + getGraphName = () => 'businessAttribute'; + getCustomCardUrlPath = () => PageRoutes.BUSINESS_ATTRIBUTE; isBrowseEnabled = () => false; diff --git a/datahub-web-react/src/app/entity/chart/ChartEntity.tsx b/datahub-web-react/src/app/entity/chart/ChartEntity.tsx index 913d502972fe1..8a62a9018661e 100644 --- a/datahub-web-react/src/app/entity/chart/ChartEntity.tsx +++ b/datahub-web-react/src/app/entity/chart/ChartEntity.tsx @@ -69,6 +69,8 @@ export class ChartEntity implements Entity { getAutoCompleteFieldName = () => 'title'; + getGraphName = () => 'chart'; + getPathName = () => 'chart'; getEntityName = () => 'Chart'; diff --git a/datahub-web-react/src/app/entity/container/ContainerEntity.tsx b/datahub-web-react/src/app/entity/container/ContainerEntity.tsx index 89f9122c6287f..941e7fc3f552d 100644 --- a/datahub-web-react/src/app/entity/container/ContainerEntity.tsx +++ b/datahub-web-react/src/app/entity/container/ContainerEntity.tsx @@ -59,6 +59,8 @@ export class ContainerEntity implements Entity { getAutoCompleteFieldName = () => 'name'; + getGraphName = () => 'container'; + getPathName = () => 'container'; getEntityName = () => 'Container'; diff --git a/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx b/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx index 9564cbc18198e..95d4431d59179 100644 --- a/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx +++ b/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx @@ -291,6 +291,8 @@ export class DashboardEntity implements Entity { ]); }; + getGraphName = () => this.getPathName(); + renderEmbeddedProfile = (urn: string) => ( { getAutoCompleteFieldName = () => 'name'; + getGraphName = () => 'dataFlow'; + getPathName = () => 'pipelines'; getEntityName = () => 'Pipeline'; diff --git a/datahub-web-react/src/app/entity/dataJob/DataJobEntity.tsx b/datahub-web-react/src/app/entity/dataJob/DataJobEntity.tsx index fe1a906371e9d..6bf9548226919 100644 --- a/datahub-web-react/src/app/entity/dataJob/DataJobEntity.tsx +++ b/datahub-web-react/src/app/entity/dataJob/DataJobEntity.tsx @@ -64,6 +64,8 @@ export class DataJobEntity implements Entity { getAutoCompleteFieldName = () => 'name'; + getGraphName = () => 'dataJob'; + getPathName = () => 'tasks'; getEntityName = () => 'Task'; diff --git a/datahub-web-react/src/app/entity/dataPlatform/DataPlatformEntity.tsx b/datahub-web-react/src/app/entity/dataPlatform/DataPlatformEntity.tsx index 6687ec9f914c1..89cbaf3cbeaa1 100644 --- a/datahub-web-react/src/app/entity/dataPlatform/DataPlatformEntity.tsx +++ b/datahub-web-react/src/app/entity/dataPlatform/DataPlatformEntity.tsx @@ -71,4 +71,8 @@ export class DataPlatformEntity implements Entity { supportedCapabilities = () => { return new Set([]); }; + + getGraphName = () => { + return 'dataPlatform'; + }; } diff --git a/datahub-web-react/src/app/entity/dataPlatformInstance/DataPlatformInstanceEntity.tsx b/datahub-web-react/src/app/entity/dataPlatformInstance/DataPlatformInstanceEntity.tsx index a542e1b52f510..d0db687ffed92 100644 --- a/datahub-web-react/src/app/entity/dataPlatformInstance/DataPlatformInstanceEntity.tsx +++ b/datahub-web-react/src/app/entity/dataPlatformInstance/DataPlatformInstanceEntity.tsx @@ -58,4 +58,8 @@ export class DataPlatformInstanceEntity implements Entity supportedCapabilities = () => { return new Set([]); }; + + getGraphName = () => { + return 'dataPlatformInstance'; + }; } diff --git a/datahub-web-react/src/app/entity/dataProduct/DataProductEntity.tsx b/datahub-web-react/src/app/entity/dataProduct/DataProductEntity.tsx index 6b31de84f85bb..90c1127d9a5fc 100644 --- a/datahub-web-react/src/app/entity/dataProduct/DataProductEntity.tsx +++ b/datahub-web-react/src/app/entity/dataProduct/DataProductEntity.tsx @@ -191,4 +191,8 @@ export class DataProductEntity implements Entity { EntityCapabilityType.DOMAINS, ]); }; + + getGraphName = () => { + return 'dataProduct'; + }; } diff --git a/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx b/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx index 21ae085832cb3..07ab27a38f889 100644 --- a/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx +++ b/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx @@ -85,6 +85,8 @@ export class DatasetEntity implements Entity { getPathName = () => 'dataset'; + getGraphName = () => 'dataset'; + getEntityName = () => 'Dataset'; getCollectionName = () => 'Datasets'; diff --git a/datahub-web-react/src/app/entity/domain/DomainEntity.tsx b/datahub-web-react/src/app/entity/domain/DomainEntity.tsx index 2b67c88a6ff23..81d245c230843 100644 --- a/datahub-web-react/src/app/entity/domain/DomainEntity.tsx +++ b/datahub-web-react/src/app/entity/domain/DomainEntity.tsx @@ -60,6 +60,8 @@ export class DomainEntity implements Entity { getAutoCompleteFieldName = () => 'name'; + getGraphName = () => 'domain'; + getPathName = () => 'domain'; getEntityName = () => 'Domain'; diff --git a/datahub-web-react/src/app/entity/ermodelrelationships/ERModelRelationshipEntity.tsx b/datahub-web-react/src/app/entity/ermodelrelationships/ERModelRelationshipEntity.tsx index aece3db1312af..3eb950cb0e7ac 100644 --- a/datahub-web-react/src/app/entity/ermodelrelationships/ERModelRelationshipEntity.tsx +++ b/datahub-web-react/src/app/entity/ermodelrelationships/ERModelRelationshipEntity.tsx @@ -58,6 +58,8 @@ export class ERModelRelationshipEntity implements Entity { getEntityName = () => 'ER-Model-Relationship'; + getGraphName = () => 'erModelRelationship'; + renderProfile = (urn: string) => ( { EntityCapabilityType.SOFT_DELETE, ]); }; + + getGraphName = () => this.getPathName(); } export default GlossaryNodeEntity; diff --git a/datahub-web-react/src/app/entity/glossaryTerm/GlossaryTermEntity.tsx b/datahub-web-react/src/app/entity/glossaryTerm/GlossaryTermEntity.tsx index 8bbc0a693b223..73c5a8e12122d 100644 --- a/datahub-web-react/src/app/entity/glossaryTerm/GlossaryTermEntity.tsx +++ b/datahub-web-react/src/app/entity/glossaryTerm/GlossaryTermEntity.tsx @@ -179,4 +179,6 @@ export class GlossaryTermEntity implements Entity { EntityCapabilityType.SOFT_DELETE, ]); }; + + getGraphName = () => this.getPathName(); } diff --git a/datahub-web-react/src/app/entity/group/Group.tsx b/datahub-web-react/src/app/entity/group/Group.tsx index cd9cf1ca6eec4..763db856f33ac 100644 --- a/datahub-web-react/src/app/entity/group/Group.tsx +++ b/datahub-web-react/src/app/entity/group/Group.tsx @@ -40,6 +40,8 @@ export class GroupEntity implements Entity { getAutoCompleteFieldName = () => 'name'; + getGraphName: () => string = () => 'corpGroup'; + getPathName: () => string = () => 'group'; getEntityName = () => 'Group'; diff --git a/datahub-web-react/src/app/entity/mlFeature/MLFeatureEntity.tsx b/datahub-web-react/src/app/entity/mlFeature/MLFeatureEntity.tsx index 2f2786b1c0d96..eecffdb2f3843 100644 --- a/datahub-web-react/src/app/entity/mlFeature/MLFeatureEntity.tsx +++ b/datahub-web-react/src/app/entity/mlFeature/MLFeatureEntity.tsx @@ -52,6 +52,8 @@ export class MLFeatureEntity implements Entity { getAutoCompleteFieldName = () => 'name'; + getGraphName = () => 'mlFeature'; + getPathName = () => 'features'; getEntityName = () => 'Feature'; diff --git a/datahub-web-react/src/app/entity/mlFeatureTable/MLFeatureTableEntity.tsx b/datahub-web-react/src/app/entity/mlFeatureTable/MLFeatureTableEntity.tsx index 595c73fbc3cb6..8aa0c056b716f 100644 --- a/datahub-web-react/src/app/entity/mlFeatureTable/MLFeatureTableEntity.tsx +++ b/datahub-web-react/src/app/entity/mlFeatureTable/MLFeatureTableEntity.tsx @@ -53,6 +53,8 @@ export class MLFeatureTableEntity implements Entity { getAutoCompleteFieldName = () => 'name'; + getGraphName = () => 'mlFeatureTable'; + getPathName = () => 'featureTables'; getEntityName = () => 'Feature Table'; diff --git a/datahub-web-react/src/app/entity/mlModel/MLModelEntity.tsx b/datahub-web-react/src/app/entity/mlModel/MLModelEntity.tsx index d4d0b37da9ec9..92f03aaef7a17 100644 --- a/datahub-web-react/src/app/entity/mlModel/MLModelEntity.tsx +++ b/datahub-web-react/src/app/entity/mlModel/MLModelEntity.tsx @@ -52,6 +52,8 @@ export class MLModelEntity implements Entity { getAutoCompleteFieldName = () => 'name'; + getGraphName = () => 'mlModel'; + getPathName = () => 'mlModels'; getEntityName = () => 'ML Model'; diff --git a/datahub-web-react/src/app/entity/mlModelGroup/MLModelGroupEntity.tsx b/datahub-web-react/src/app/entity/mlModelGroup/MLModelGroupEntity.tsx index 5896c1864cc43..b5d32275f97bf 100644 --- a/datahub-web-react/src/app/entity/mlModelGroup/MLModelGroupEntity.tsx +++ b/datahub-web-react/src/app/entity/mlModelGroup/MLModelGroupEntity.tsx @@ -50,6 +50,8 @@ export class MLModelGroupEntity implements Entity { getAutoCompleteFieldName = () => 'name'; + getGraphName = () => 'mlModelGroup'; + getPathName = () => 'mlModelGroup'; getEntityName = () => 'ML Group'; diff --git a/datahub-web-react/src/app/entity/mlPrimaryKey/MLPrimaryKeyEntity.tsx b/datahub-web-react/src/app/entity/mlPrimaryKey/MLPrimaryKeyEntity.tsx index 60c7531a4f57c..119a566b04f13 100644 --- a/datahub-web-react/src/app/entity/mlPrimaryKey/MLPrimaryKeyEntity.tsx +++ b/datahub-web-react/src/app/entity/mlPrimaryKey/MLPrimaryKeyEntity.tsx @@ -51,6 +51,8 @@ export class MLPrimaryKeyEntity implements Entity { getAutoCompleteFieldName = () => 'name'; + getGraphName = () => 'mlPrimaryKey'; + getPathName = () => 'mlPrimaryKeys'; getEntityName = () => 'ML Primary Key'; diff --git a/datahub-web-react/src/app/entity/schemaField/SchemaFieldPropertiesEntity.tsx b/datahub-web-react/src/app/entity/schemaField/SchemaFieldPropertiesEntity.tsx index 88743012ddbc8..2c59c476195d0 100644 --- a/datahub-web-react/src/app/entity/schemaField/SchemaFieldPropertiesEntity.tsx +++ b/datahub-web-react/src/app/entity/schemaField/SchemaFieldPropertiesEntity.tsx @@ -44,6 +44,8 @@ export class SchemaFieldPropertiesEntity implements Entity { // Currently unused. renderProfile = (_: string) => <>; + getGraphName = () => 'schemaField'; + renderPreview = (previewType: PreviewType, data: SchemaFieldEntity) => { const parent = data.parent as Dataset; return ( diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/useStructuredProperties.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/useStructuredProperties.tsx index 86365b8232905..4635486c24d1d 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Properties/useStructuredProperties.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/useStructuredProperties.tsx @@ -23,6 +23,25 @@ export function mapStructuredPropertyValues(structuredPropertiesEntry: Structure })); } +export function mapStructuredPropertyToPropertyRow(structuredPropertiesEntry: StructuredPropertiesEntry) { + const { displayName, qualifiedName } = structuredPropertiesEntry.structuredProperty.definition; + return { + displayName: displayName || qualifiedName, + qualifiedName, + values: mapStructuredPropertyValues(structuredPropertiesEntry), + dataType: structuredPropertiesEntry.structuredProperty.definition.valueType, + structuredProperty: structuredPropertiesEntry.structuredProperty, + type: + structuredPropertiesEntry.values[0] && structuredPropertiesEntry.values[0].__typename + ? { + type: typeNameToType[structuredPropertiesEntry.values[0].__typename].type, + nativeDataType: typeNameToType[structuredPropertiesEntry.values[0].__typename].nativeDataType, + } + : undefined, + associatedUrn: structuredPropertiesEntry.associatedUrn, + }; +} + // map the properties map into a list of PropertyRow objects to render in a table function getStructuredPropertyRows(entityData?: GenericEntityProperties | null) { const structuredPropertyRows: PropertyRow[] = []; diff --git a/datahub-web-react/src/app/entity/structuredProperty/StructuredPropertyEntity.tsx b/datahub-web-react/src/app/entity/structuredProperty/StructuredPropertyEntity.tsx new file mode 100644 index 0000000000000..2bdfd550652fa --- /dev/null +++ b/datahub-web-react/src/app/entity/structuredProperty/StructuredPropertyEntity.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; +import styled from 'styled-components'; +import TableIcon from '@src/images/table-icon.svg?react'; +import { TYPE_ICON_CLASS_NAME } from '@src/app/shared/constants'; +import DefaultPreviewCard from '@src/app/preview/DefaultPreviewCard'; +import { EntityType, SearchResult, StructuredPropertyEntity as StructuredProperty } from '../../../types.generated'; +import { Entity, IconStyleType, PreviewType } from '../Entity'; +import { getDataForEntityType } from '../shared/containers/profile/utils'; +import { urlEncodeUrn } from '../shared/utils'; + +const PreviewPropIcon = styled(TableIcon)` + font-size: 20px; +`; + +/** + * Definition of the DataHub Structured Property entity. + */ +export class StructuredPropertyEntity implements Entity { + type: EntityType = EntityType.StructuredProperty; + + icon = (fontSize?: number, styleType?: IconStyleType, color?: string) => { + if (styleType === IconStyleType.TAB_VIEW) { + return ; + } + + if (styleType === IconStyleType.HIGHLIGHT) { + return ; + } + + return ( + + ); + }; + + isSearchEnabled = () => false; + + isBrowseEnabled = () => false; + + isLineageEnabled = () => false; + + getAutoCompleteFieldName = () => 'name'; + + getGraphName = () => 'structuredProperty'; + + getPathName: () => string = () => this.getGraphName(); + + getCollectionName: () => string = () => 'Structured Properties'; + + getEntityName: () => string = () => 'Structured Property'; + + renderProfile: (urn: string) => JSX.Element = (_urn) =>
; // not used right now + + renderPreview = (previewType: PreviewType, data: StructuredProperty) => ( + } + typeIcon={this.icon(14, IconStyleType.ACCENT)} + previewType={previewType} + /> + ); + + renderSearch = (result: SearchResult) => { + return this.renderPreview(PreviewType.SEARCH, result.entity as StructuredProperty); + }; + + displayName = (data: StructuredProperty) => { + return data.definition?.displayName || data.definition?.qualifiedName || data.urn; + }; + + getGenericEntityProperties = (entity: StructuredProperty) => { + return getDataForEntityType({ data: entity, entityType: this.type, getOverrideProperties: (data) => data }); + }; + + supportedCapabilities = () => { + return new Set([]); + }; +} diff --git a/datahub-web-react/src/app/entity/tag/Tag.tsx b/datahub-web-react/src/app/entity/tag/Tag.tsx index 6f0839e5f812b..d3c5b07966099 100644 --- a/datahub-web-react/src/app/entity/tag/Tag.tsx +++ b/datahub-web-react/src/app/entity/tag/Tag.tsx @@ -46,6 +46,8 @@ export class TagEntity implements Entity { getAutoCompleteFieldName = () => 'name'; + getGraphName = () => 'tag'; + getPathName: () => string = () => 'tag'; getCollectionName: () => string = () => 'Tags'; diff --git a/datahub-web-react/src/app/entity/user/User.tsx b/datahub-web-react/src/app/entity/user/User.tsx index ec1c5fbdc8698..058349f83eaec 100644 --- a/datahub-web-react/src/app/entity/user/User.tsx +++ b/datahub-web-react/src/app/entity/user/User.tsx @@ -39,6 +39,8 @@ export class UserEntity implements Entity { getAutoCompleteFieldName = () => 'username'; + getGraphName: () => string = () => 'corpuser'; + getPathName: () => string = () => 'user'; getEntityName = () => 'Person'; diff --git a/datahub-web-react/src/app/govern/structuredProperties/AdvancedOptions.tsx b/datahub-web-react/src/app/govern/structuredProperties/AdvancedOptions.tsx new file mode 100644 index 0000000000000..620143258ef5f --- /dev/null +++ b/datahub-web-react/src/app/govern/structuredProperties/AdvancedOptions.tsx @@ -0,0 +1,64 @@ +import { Icon, Input, Text, Tooltip } from '@components'; +import { Collapse, Form } from 'antd'; +import React from 'react'; +import { CollapseHeader, FlexContainer, InputLabel, StyledCollapse } from './styledComponents'; + +interface Props { + isEditMode: boolean; +} + +const AdvancedOptions = ({ isEditMode }: Props) => { + return ( + ( + + )} + expandIconPosition="end" + defaultActiveKey={[]} + > + + + Advanced Options + + + } + forceRender + > + + + Qualified Name + + + + + + + + + + + + + ); +}; + +export default AdvancedOptions; diff --git a/datahub-web-react/src/app/govern/structuredProperties/AllowedValuesDrawer.tsx b/datahub-web-react/src/app/govern/structuredProperties/AllowedValuesDrawer.tsx new file mode 100644 index 0000000000000..f1dccb6db0c22 --- /dev/null +++ b/datahub-web-react/src/app/govern/structuredProperties/AllowedValuesDrawer.tsx @@ -0,0 +1,142 @@ +import { Button, Icon, Input, Text, TextArea } from '@src/alchemy-components'; +import { AllowedValue } from '@src/types.generated'; +import { Form, FormInstance } from 'antd'; +import { Tooltip } from '@components'; +import React, { useEffect, useRef } from 'react'; +import { + AddButtonContainer, + DeleteIconContainer, + FieldGroupContainer, + FormContainer, + InputLabel, + StyledDivider, + ValuesContainer, +} from './styledComponents'; +import { PropValueField } from './utils'; + +interface Props { + showAllowedValuesDrawer: boolean; + propType: PropValueField; + allowedValues: AllowedValue[] | undefined; + isEditMode: boolean; + noOfExistingValues: number; + form: FormInstance; +} + +const AllowedValuesDrawer = ({ + showAllowedValuesDrawer, + propType, + allowedValues, + isEditMode, + noOfExistingValues, + form, +}: Props) => { + useEffect(() => { + form.setFieldsValue({ allowedValues: allowedValues || [{}] }); + }, [form, showAllowedValuesDrawer, allowedValues]); + + const containerRef = useRef(null); + + // Scroll to the bottom to show the newly added fields + const scrollToBottom = () => { + if (containerRef.current) { + containerRef.current.scrollTop = containerRef.current.scrollHeight; + } + }; + + return ( +
+ + {(fields, { add, remove }) => ( + + {fields.length > 0 && ( + + {fields.map((field, index) => { + const isExisting = isEditMode && index < noOfExistingValues; + + return ( + + + Value + + * + + + + + + + + +