feat(application): editing application assignment via UI (#13739)

This commit is contained in:
Gabe Lyons 2025-06-11 09:26:40 -04:00 committed by GitHub
parent 6b8dfb0aa8
commit 476ac881a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 893 additions and 11 deletions

View File

@ -2864,6 +2864,18 @@ public class GmsGraphQLEngine {
.dataFetcher(
"aspects", new WeaklyTypedAspectsResolver(entityClient, entityRegistry))
.dataFetcher("relationships", new EntityRelationshipsResultResolver(graphClient)));
builder.type(
"ApplicationAssociation",
typeWiring ->
typeWiring.dataFetcher(
"application",
new LoadableTypeResolver<>(
applicationType,
(env) ->
((com.linkedin.datahub.graphql.generated.ApplicationAssociation)
env.getSource())
.getApplication()
.getUrn())));
}
private void configureAssertionResolvers(final RuntimeWiring.Builder builder) {

View File

@ -6,6 +6,7 @@ import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.featureflags.FeatureFlags;
import com.linkedin.datahub.graphql.generated.AnalyticsConfig;
import com.linkedin.datahub.graphql.generated.AppConfig;
import com.linkedin.datahub.graphql.generated.ApplicationConfig;
import com.linkedin.datahub.graphql.generated.AuthConfig;
import com.linkedin.datahub.graphql.generated.ChromeExtensionConfig;
import com.linkedin.datahub.graphql.generated.EntityProfileConfig;
@ -187,6 +188,12 @@ public class AppConfigResolver implements DataFetcher<CompletableFuture<AppConfi
}
visualConfig.setTheme(themeConfig);
}
if (_visualConfiguration != null && _visualConfiguration.getApplication() != null) {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setShowSidebarSectionWhenEmpty(
_visualConfiguration.getApplication().isShowSidebarSectionWhenEmpty());
visualConfig.setApplication(applicationConfig);
}
appConfig.setVisualConfig(visualConfig);
final TelemetryConfig telemetryConfig = new TelemetryConfig();

View File

@ -0,0 +1,46 @@
package com.linkedin.datahub.graphql.types.application;
import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.canView;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.generated.Application;
import com.linkedin.datahub.graphql.generated.ApplicationAssociation;
import com.linkedin.datahub.graphql.generated.EntityType;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Maps Pegasus {@link RecordTemplate} objects to objects conforming to the GQL schema.
*
* <p>To be replaced by auto-generated mappers implementations
*/
public class ApplicationAssociationMapper {
public static final ApplicationAssociationMapper INSTANCE = new ApplicationAssociationMapper();
public static ApplicationAssociation map(
@Nullable final QueryContext context,
@Nonnull final com.linkedin.application.Applications applications,
@Nonnull final String entityUrn) {
return INSTANCE.apply(context, applications, entityUrn);
}
public ApplicationAssociation apply(
@Nullable final QueryContext context,
@Nonnull final com.linkedin.application.Applications applications,
@Nonnull final String entityUrn) {
if (applications.getApplications().size() > 0
&& (context == null
|| canView(context.getOperationContext(), applications.getApplications().get(0)))) {
ApplicationAssociation association = new ApplicationAssociation();
association.setApplication(
Application.builder()
.setType(EntityType.APPLICATION)
.setUrn(applications.getApplications().get(0).toString())
.build());
association.setAssociatedUrn(entityUrn);
return association;
}
return null;
}
}

View File

@ -82,7 +82,8 @@ public class ChartType
BROWSE_PATHS_V2_ASPECT_NAME,
SUB_TYPES_ASPECT_NAME,
STRUCTURED_PROPERTIES_ASPECT_NAME,
FORMS_ASPECT_NAME);
FORMS_ASPECT_NAME,
APPLICATION_MEMBERSHIP_ASPECT_NAME);
private static final Set<String> FACET_FIELDS =
ImmutableSet.of("access", "queryType", "tool", "type");

View File

@ -3,6 +3,7 @@ package com.linkedin.datahub.graphql.types.chart.mappers;
import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.canView;
import static com.linkedin.metadata.Constants.*;
import com.linkedin.application.Applications;
import com.linkedin.chart.EditableChartProperties;
import com.linkedin.common.BrowsePathsV2;
import com.linkedin.common.DataPlatformInstance;
@ -32,6 +33,7 @@ import com.linkedin.datahub.graphql.generated.Container;
import com.linkedin.datahub.graphql.generated.DataPlatform;
import com.linkedin.datahub.graphql.generated.Dataset;
import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.types.application.ApplicationAssociationMapper;
import com.linkedin.datahub.graphql.types.common.mappers.AuditStampMapper;
import com.linkedin.datahub.graphql.types.common.mappers.BrowsePathsV2Mapper;
import com.linkedin.datahub.graphql.types.common.mappers.CustomPropertiesMapper;
@ -148,6 +150,9 @@ public class ChartMapper implements ModelMapper<EntityResponse, Chart> {
FORMS_ASPECT_NAME,
((entity, dataMap) ->
entity.setForms(FormsMapper.map(new Forms(dataMap), entityUrn.toString()))));
mappingHelper.mapToResult(
APPLICATION_MEMBERSHIP_ASPECT_NAME,
(chart, dataMap) -> mapApplicationAssociation(context, chart, dataMap));
if (context != null && !canView(context.getOperationContext(), entityUrn)) {
return AuthorizationUtils.restrictEntity(mappingHelper.getResult(), Chart.class);
@ -305,4 +310,10 @@ public class ChartMapper implements ModelMapper<EntityResponse, Chart> {
final Domains domains = new Domains(dataMap);
chart.setDomain(DomainAssociationMapper.map(context, domains, chart.getUrn()));
}
private static void mapApplicationAssociation(
@Nullable final QueryContext context, @Nonnull Chart chart, @Nonnull DataMap dataMap) {
final Applications applications = new Applications(dataMap);
chart.setApplication(ApplicationAssociationMapper.map(context, applications, chart.getUrn()));
}
}

View File

@ -82,7 +82,8 @@ public class DashboardType
DATA_PRODUCTS_ASPECT_NAME,
BROWSE_PATHS_V2_ASPECT_NAME,
STRUCTURED_PROPERTIES_ASPECT_NAME,
FORMS_ASPECT_NAME);
FORMS_ASPECT_NAME,
APPLICATION_MEMBERSHIP_ASPECT_NAME);
private static final Set<String> FACET_FIELDS = ImmutableSet.of("access", "tool");
private final EntityClient _entityClient;

View File

@ -3,6 +3,7 @@ package com.linkedin.datahub.graphql.types.dashboard.mappers;
import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.canView;
import static com.linkedin.metadata.Constants.*;
import com.linkedin.application.Applications;
import com.linkedin.common.BrowsePathsV2;
import com.linkedin.common.DataPlatformInstance;
import com.linkedin.common.Deprecation;
@ -29,6 +30,7 @@ import com.linkedin.datahub.graphql.generated.DashboardInfo;
import com.linkedin.datahub.graphql.generated.DashboardProperties;
import com.linkedin.datahub.graphql.generated.DataPlatform;
import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.types.application.ApplicationAssociationMapper;
import com.linkedin.datahub.graphql.types.chart.mappers.InputFieldsMapper;
import com.linkedin.datahub.graphql.types.common.mappers.AuditStampMapper;
import com.linkedin.datahub.graphql.types.common.mappers.BrowsePathsV2Mapper;
@ -148,6 +150,9 @@ public class DashboardMapper implements ModelMapper<EntityResponse, Dashboard> {
FORMS_ASPECT_NAME,
((entity, dataMap) ->
entity.setForms(FormsMapper.map(new Forms(dataMap), entityUrn.toString()))));
mappingHelper.mapToResult(
APPLICATION_MEMBERSHIP_ASPECT_NAME,
(dashboard, dataMap) -> mapApplicationAssociation(context, dashboard, dataMap));
if (context != null && !canView(context.getOperationContext(), entityUrn)) {
return AuthorizationUtils.restrictEntity(mappingHelper.getResult(), Dashboard.class);
@ -297,4 +302,13 @@ public class DashboardMapper implements ModelMapper<EntityResponse, Dashboard> {
final Domains domains = new Domains(dataMap);
dashboard.setDomain(DomainAssociationMapper.map(context, domains, dashboard.getUrn()));
}
private static void mapApplicationAssociation(
@Nullable final QueryContext context,
@Nonnull Dashboard dashboard,
@Nonnull DataMap dataMap) {
final Applications applications = new Applications(dataMap);
dashboard.setApplication(
ApplicationAssociationMapper.map(context, applications, dashboard.getUrn()));
}
}

View File

@ -79,7 +79,8 @@ public class DataFlowType
BROWSE_PATHS_V2_ASPECT_NAME,
STRUCTURED_PROPERTIES_ASPECT_NAME,
SUB_TYPES_ASPECT_NAME,
FORMS_ASPECT_NAME);
FORMS_ASPECT_NAME,
APPLICATION_MEMBERSHIP_ASPECT_NAME);
private static final Set<String> FACET_FIELDS = ImmutableSet.of("orchestrator", "cluster");
private final EntityClient _entityClient;

View File

@ -3,6 +3,7 @@ package com.linkedin.datahub.graphql.types.dataflow.mappers;
import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.canView;
import static com.linkedin.metadata.Constants.*;
import com.linkedin.application.Applications;
import com.linkedin.common.BrowsePathsV2;
import com.linkedin.common.DataPlatformInstance;
import com.linkedin.common.Deprecation;
@ -24,6 +25,7 @@ import com.linkedin.datahub.graphql.generated.DataFlowInfo;
import com.linkedin.datahub.graphql.generated.DataFlowProperties;
import com.linkedin.datahub.graphql.generated.DataPlatform;
import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.types.application.ApplicationAssociationMapper;
import com.linkedin.datahub.graphql.types.common.mappers.BrowsePathsV2Mapper;
import com.linkedin.datahub.graphql.types.common.mappers.CustomPropertiesMapper;
import com.linkedin.datahub.graphql.types.common.mappers.DataPlatformInstanceAspectMapper;
@ -128,6 +130,9 @@ public class DataFlowMapper implements ModelMapper<EntityResponse, DataFlow> {
SUB_TYPES_ASPECT_NAME,
(entity, dataMap) ->
entity.setSubTypes(SubTypesMapper.map(context, new SubTypes(dataMap))));
mappingHelper.mapToResult(
APPLICATION_MEMBERSHIP_ASPECT_NAME,
(dataFlow, dataMap) -> mapApplicationAssociation(context, dataFlow, dataMap));
if (context != null && !canView(context.getOperationContext(), entityUrn)) {
return AuthorizationUtils.restrictEntity(mappingHelper.getResult(), DataFlow.class);
@ -231,4 +236,11 @@ public class DataFlowMapper implements ModelMapper<EntityResponse, DataFlow> {
// Currently we only take the first domain if it exists.
dataFlow.setDomain(DomainAssociationMapper.map(context, domains, dataFlow.getUrn()));
}
private static void mapApplicationAssociation(
@Nullable final QueryContext context, @Nonnull DataFlow dataFlow, @Nonnull DataMap dataMap) {
final Applications applications = new Applications(dataMap);
dataFlow.setApplication(
ApplicationAssociationMapper.map(context, applications, dataFlow.getUrn()));
}
}

View File

@ -81,7 +81,8 @@ public class DataJobType
SUB_TYPES_ASPECT_NAME,
STRUCTURED_PROPERTIES_ASPECT_NAME,
FORMS_ASPECT_NAME,
DATA_TRANSFORM_LOGIC_ASPECT_NAME);
DATA_TRANSFORM_LOGIC_ASPECT_NAME,
APPLICATION_MEMBERSHIP_ASPECT_NAME);
private static final Set<String> FACET_FIELDS = ImmutableSet.of("flow");
private final EntityClient _entityClient;

View File

@ -4,6 +4,7 @@ import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.canV
import static com.linkedin.metadata.Constants.*;
import com.google.common.collect.ImmutableList;
import com.linkedin.application.Applications;
import com.linkedin.common.*;
import com.linkedin.common.urn.Urn;
import com.linkedin.data.DataMap;
@ -18,6 +19,7 @@ import com.linkedin.datahub.graphql.generated.DataJobInputOutput;
import com.linkedin.datahub.graphql.generated.DataJobProperties;
import com.linkedin.datahub.graphql.generated.Dataset;
import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.types.application.ApplicationAssociationMapper;
import com.linkedin.datahub.graphql.types.common.mappers.*;
import com.linkedin.datahub.graphql.types.common.mappers.util.SystemMetadataUtils;
import com.linkedin.datahub.graphql.types.domain.DomainAssociationMapper;
@ -137,6 +139,8 @@ public class DataJobMapper implements ModelMapper<EntityResponse, DataJob> {
}
});
mapApplicationAssociation(context, entityResponse.getAspects(), result);
if (context != null && !canView(context.getOperationContext(), entityUrn)) {
return AuthorizationUtils.restrictEntity(result, DataJob.class);
} else {
@ -223,4 +227,14 @@ public class DataJobMapper implements ModelMapper<EntityResponse, DataJob> {
return result;
}
private void mapApplicationAssociation(
final QueryContext context, final EnvelopedAspectMap aspectMap, final DataJob dataJob) {
if (aspectMap.containsKey(APPLICATION_MEMBERSHIP_ASPECT_NAME)) {
final Applications applications =
new Applications(aspectMap.get(APPLICATION_MEMBERSHIP_ASPECT_NAME).getValue().data());
dataJob.setApplication(
ApplicationAssociationMapper.map(context, applications, dataJob.getUrn()));
}
}
}

View File

@ -1,5 +1,6 @@
package com.linkedin.datahub.graphql.types.dataproduct;
import static com.linkedin.metadata.Constants.APPLICATION_MEMBERSHIP_ASPECT_NAME;
import static com.linkedin.metadata.Constants.DATA_PRODUCT_ENTITY_NAME;
import static com.linkedin.metadata.Constants.DATA_PRODUCT_PROPERTIES_ASPECT_NAME;
import static com.linkedin.metadata.Constants.DOMAINS_ASPECT_NAME;
@ -53,7 +54,8 @@ public class DataProductType
DOMAINS_ASPECT_NAME,
INSTITUTIONAL_MEMORY_ASPECT_NAME,
STRUCTURED_PROPERTIES_ASPECT_NAME,
FORMS_ASPECT_NAME);
FORMS_ASPECT_NAME,
APPLICATION_MEMBERSHIP_ASPECT_NAME);
private final EntityClient _entityClient;
@Override

View File

@ -1,6 +1,7 @@
package com.linkedin.datahub.graphql.types.dataproduct.mappers;
import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.canView;
import static com.linkedin.metadata.Constants.APPLICATION_MEMBERSHIP_ASPECT_NAME;
import static com.linkedin.metadata.Constants.DATA_PRODUCT_PROPERTIES_ASPECT_NAME;
import static com.linkedin.metadata.Constants.DOMAINS_ASPECT_NAME;
import static com.linkedin.metadata.Constants.FORMS_ASPECT_NAME;
@ -10,6 +11,7 @@ import static com.linkedin.metadata.Constants.INSTITUTIONAL_MEMORY_ASPECT_NAME;
import static com.linkedin.metadata.Constants.OWNERSHIP_ASPECT_NAME;
import static com.linkedin.metadata.Constants.STRUCTURED_PROPERTIES_ASPECT_NAME;
import com.linkedin.application.Applications;
import com.linkedin.common.Forms;
import com.linkedin.common.GlobalTags;
import com.linkedin.common.GlossaryTerms;
@ -22,6 +24,7 @@ import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.generated.DataProduct;
import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.types.application.ApplicationAssociationMapper;
import com.linkedin.datahub.graphql.types.common.mappers.CustomPropertiesMapper;
import com.linkedin.datahub.graphql.types.common.mappers.InstitutionalMemoryMapper;
import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper;
@ -98,6 +101,9 @@ public class DataProductMapper implements ModelMapper<EntityResponse, DataProduc
FORMS_ASPECT_NAME,
((entity, dataMap) ->
entity.setForms(FormsMapper.map(new Forms(dataMap), entityUrn.toString()))));
mappingHelper.mapToResult(
APPLICATION_MEMBERSHIP_ASPECT_NAME,
(dataProduct, dataMap) -> mapApplicationAssociation(context, dataProduct, dataMap));
if (context != null && !canView(context.getOperationContext(), entityUrn)) {
return AuthorizationUtils.restrictEntity(result, DataProduct.class);
@ -130,4 +136,13 @@ public class DataProductMapper implements ModelMapper<EntityResponse, DataProduc
dataProduct.setProperties(properties);
}
private static void mapApplicationAssociation(
@Nullable final QueryContext context,
@Nonnull DataProduct dataProduct,
@Nonnull DataMap dataMap) {
final Applications applications = new Applications(dataMap);
dataProduct.setApplication(
ApplicationAssociationMapper.map(context, applications, dataProduct.getUrn()));
}
}

View File

@ -90,6 +90,7 @@ public class DatasetType
STRUCTURED_PROPERTIES_ASPECT_NAME,
FORMS_ASPECT_NAME,
SUB_TYPES_ASPECT_NAME,
APPLICATION_MEMBERSHIP_ASPECT_NAME,
VERSION_PROPERTIES_ASPECT_NAME);
private static final Set<String> FACET_FIELDS = ImmutableSet.of("origin", "platform");

View File

@ -3,6 +3,7 @@ package com.linkedin.datahub.graphql.types.dataset.mappers;
import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.canView;
import static com.linkedin.metadata.Constants.*;
import com.linkedin.application.Applications;
import com.linkedin.common.Access;
import com.linkedin.common.BrowsePathsV2;
import com.linkedin.common.DataPlatformInstance;
@ -29,6 +30,7 @@ import com.linkedin.datahub.graphql.generated.Dataset;
import com.linkedin.datahub.graphql.generated.DatasetEditableProperties;
import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.generated.FabricType;
import com.linkedin.datahub.graphql.types.application.ApplicationAssociationMapper;
import com.linkedin.datahub.graphql.types.common.mappers.BrowsePathsV2Mapper;
import com.linkedin.datahub.graphql.types.common.mappers.CustomPropertiesMapper;
import com.linkedin.datahub.graphql.types.common.mappers.DataPlatformInstanceAspectMapper;
@ -142,6 +144,9 @@ public class DatasetMapper implements ModelMapper<EntityResponse, Dataset> {
GlossaryTermsMapper.map(context, new GlossaryTerms(dataMap), entityUrn)));
mappingHelper.mapToResult(context, CONTAINER_ASPECT_NAME, DatasetMapper::mapContainers);
mappingHelper.mapToResult(context, DOMAINS_ASPECT_NAME, DatasetMapper::mapDomains);
mappingHelper.mapToResult(
APPLICATION_MEMBERSHIP_ASPECT_NAME,
(dataset, dataMap) -> mapApplicationAssociation(context, dataset, dataMap));
mappingHelper.mapToResult(
DEPRECATION_ASPECT_NAME,
(dataset, dataMap) ->
@ -151,6 +156,8 @@ public class DatasetMapper implements ModelMapper<EntityResponse, Dataset> {
(dataset, dataMap) ->
dataset.setDataPlatformInstance(
DataPlatformInstanceAspectMapper.map(context, new DataPlatformInstance(dataMap))));
mappingHelper.mapToResult(
"applications", (dataset, dataMap) -> mapApplicationAssociation(context, dataset, dataMap));
mappingHelper.mapToResult(
SIBLINGS_ASPECT_NAME,
(dataset, dataMap) ->
@ -301,4 +308,11 @@ public class DatasetMapper implements ModelMapper<EntityResponse, Dataset> {
final Domains domains = new Domains(dataMap);
dataset.setDomain(DomainAssociationMapper.map(context, domains, dataset.getUrn()));
}
private static void mapApplicationAssociation(
@Nullable final QueryContext context, @Nonnull Dataset dataset, @Nonnull DataMap dataMap) {
final Applications applications = new Applications(dataMap);
dataset.setApplication(
ApplicationAssociationMapper.map(context, applications, dataset.getUrn()));
}
}

View File

@ -1,5 +1,6 @@
package com.linkedin.datahub.graphql.types.glossary;
import static com.linkedin.metadata.Constants.APPLICATION_MEMBERSHIP_ASPECT_NAME;
import static com.linkedin.metadata.Constants.FORMS_ASPECT_NAME;
import static com.linkedin.metadata.Constants.GLOSSARY_NODE_ENTITY_NAME;
import static com.linkedin.metadata.Constants.GLOSSARY_NODE_INFO_ASPECT_NAME;
@ -48,7 +49,8 @@ public class GlossaryNodeType
GLOSSARY_NODE_INFO_ASPECT_NAME,
OWNERSHIP_ASPECT_NAME,
STRUCTURED_PROPERTIES_ASPECT_NAME,
FORMS_ASPECT_NAME);
FORMS_ASPECT_NAME,
APPLICATION_MEMBERSHIP_ASPECT_NAME);
private final EntityClient _entityClient;

View File

@ -59,7 +59,8 @@ public class GlossaryTermType
DOMAINS_ASPECT_NAME,
DEPRECATION_ASPECT_NAME,
STRUCTURED_PROPERTIES_ASPECT_NAME,
FORMS_ASPECT_NAME);
FORMS_ASPECT_NAME,
APPLICATION_MEMBERSHIP_ASPECT_NAME);
private final EntityClient _entityClient;

View File

@ -3,6 +3,7 @@ package com.linkedin.datahub.graphql.types.glossary.mappers;
import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.canView;
import static com.linkedin.metadata.Constants.*;
import com.linkedin.application.Applications;
import com.linkedin.common.Deprecation;
import com.linkedin.common.Forms;
import com.linkedin.common.InstitutionalMemory;
@ -14,6 +15,7 @@ import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.generated.GlossaryTerm;
import com.linkedin.datahub.graphql.types.application.ApplicationAssociationMapper;
import com.linkedin.datahub.graphql.types.common.mappers.DeprecationMapper;
import com.linkedin.datahub.graphql.types.common.mappers.InstitutionalMemoryMapper;
import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper;
@ -96,6 +98,9 @@ public class GlossaryTermMapper implements ModelMapper<EntityResponse, GlossaryT
FORMS_ASPECT_NAME,
((entity, dataMap) ->
entity.setForms(FormsMapper.map(new Forms(dataMap), entityUrn.toString()))));
mappingHelper.mapToResult(
APPLICATION_MEMBERSHIP_ASPECT_NAME,
(glossaryTerm, dataMap) -> mapApplicationAssociation(context, glossaryTerm, dataMap));
// If there's no name property, resort to the legacy name computation.
if (result.getGlossaryTermInfo() != null && result.getGlossaryTermInfo().getName() == null) {
@ -124,4 +129,13 @@ public class GlossaryTermMapper implements ModelMapper<EntityResponse, GlossaryT
final Domains domains = new Domains(dataMap);
glossaryTerm.setDomain(DomainAssociationMapper.map(context, domains, glossaryTerm.getUrn()));
}
private static void mapApplicationAssociation(
@Nullable final QueryContext context,
@Nonnull GlossaryTerm glossaryTerm,
@Nonnull DataMap dataMap) {
final Applications applications = new Applications(dataMap);
glossaryTerm.setApplication(
ApplicationAssociationMapper.map(context, applications, glossaryTerm.getUrn()));
}
}

View File

@ -3,6 +3,7 @@ package com.linkedin.datahub.graphql.types.mlmodel.mappers;
import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.canView;
import static com.linkedin.metadata.Constants.*;
import com.linkedin.application.Applications;
import com.linkedin.common.BrowsePathsV2;
import com.linkedin.common.DataPlatformInstance;
import com.linkedin.common.Deprecation;
@ -21,6 +22,7 @@ import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.generated.MLFeature;
import com.linkedin.datahub.graphql.generated.MLFeatureDataType;
import com.linkedin.datahub.graphql.generated.MLFeatureEditableProperties;
import com.linkedin.datahub.graphql.types.application.ApplicationAssociationMapper;
import com.linkedin.datahub.graphql.types.common.mappers.BrowsePathsV2Mapper;
import com.linkedin.datahub.graphql.types.common.mappers.DataPlatformInstanceAspectMapper;
import com.linkedin.datahub.graphql.types.common.mappers.DeprecationMapper;
@ -122,6 +124,9 @@ public class MLFeatureMapper implements ModelMapper<EntityResponse, MLFeature> {
FORMS_ASPECT_NAME,
((entity, dataMap) ->
entity.setForms(FormsMapper.map(new Forms(dataMap), entityUrn.toString()))));
mappingHelper.mapToResult(
APPLICATION_MEMBERSHIP_ASPECT_NAME,
(mlFeature, dataMap) -> mapApplicationAssociation(context, mlFeature, dataMap));
if (context != null && !canView(context.getOperationContext(), entityUrn)) {
return AuthorizationUtils.restrictEntity(mappingHelper.getResult(), MLFeature.class);
@ -175,4 +180,13 @@ public class MLFeatureMapper implements ModelMapper<EntityResponse, MLFeature> {
}
entity.setEditableProperties(editableProperties);
}
private static void mapApplicationAssociation(
@Nullable final QueryContext context,
@Nonnull MLFeature mlFeature,
@Nonnull DataMap dataMap) {
final Applications applications = new Applications(dataMap);
mlFeature.setApplication(
ApplicationAssociationMapper.map(context, applications, mlFeature.getUrn()));
}
}

View File

@ -3,6 +3,7 @@ package com.linkedin.datahub.graphql.types.mlmodel.mappers;
import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.canView;
import static com.linkedin.metadata.Constants.*;
import com.linkedin.application.Applications;
import com.linkedin.common.BrowsePathsV2;
import com.linkedin.common.DataPlatformInstance;
import com.linkedin.common.Deprecation;
@ -21,6 +22,7 @@ import com.linkedin.datahub.graphql.generated.DataPlatform;
import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.generated.MLFeatureTable;
import com.linkedin.datahub.graphql.generated.MLFeatureTableEditableProperties;
import com.linkedin.datahub.graphql.types.application.ApplicationAssociationMapper;
import com.linkedin.datahub.graphql.types.common.mappers.BrowsePathsV2Mapper;
import com.linkedin.datahub.graphql.types.common.mappers.DataPlatformInstanceAspectMapper;
import com.linkedin.datahub.graphql.types.common.mappers.DeprecationMapper;
@ -123,6 +125,9 @@ public class MLFeatureTableMapper implements ModelMapper<EntityResponse, MLFeatu
FORMS_ASPECT_NAME,
((entity, dataMap) ->
entity.setForms(FormsMapper.map(new Forms(dataMap), entityUrn.toString()))));
mappingHelper.mapToResult(
APPLICATION_MEMBERSHIP_ASPECT_NAME,
(mlFeatureTable, dataMap) -> mapApplicationAssociation(context, mlFeatureTable, dataMap));
if (context != null && !canView(context.getOperationContext(), entityUrn)) {
return AuthorizationUtils.restrictEntity(mappingHelper.getResult(), MLFeatureTable.class);
@ -178,4 +183,13 @@ public class MLFeatureTableMapper implements ModelMapper<EntityResponse, MLFeatu
}
entity.setEditableProperties(editableProperties);
}
private static void mapApplicationAssociation(
@Nullable final QueryContext context,
@Nonnull MLFeatureTable mlFeatureTable,
@Nonnull DataMap dataMap) {
final Applications applications = new Applications(dataMap);
mlFeatureTable.setApplication(
ApplicationAssociationMapper.map(context, applications, mlFeatureTable.getUrn()));
}
}

View File

@ -3,6 +3,7 @@ package com.linkedin.datahub.graphql.types.mlmodel.mappers;
import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.canView;
import static com.linkedin.metadata.Constants.*;
import com.linkedin.application.Applications;
import com.linkedin.common.BrowsePathsV2;
import com.linkedin.common.DataPlatformInstance;
import com.linkedin.common.Deprecation;
@ -21,6 +22,7 @@ import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.generated.FabricType;
import com.linkedin.datahub.graphql.generated.MLModelGroup;
import com.linkedin.datahub.graphql.generated.MLModelGroupEditableProperties;
import com.linkedin.datahub.graphql.types.application.ApplicationAssociationMapper;
import com.linkedin.datahub.graphql.types.common.mappers.BrowsePathsV2Mapper;
import com.linkedin.datahub.graphql.types.common.mappers.DataPlatformInstanceAspectMapper;
import com.linkedin.datahub.graphql.types.common.mappers.DeprecationMapper;
@ -117,6 +119,9 @@ public class MLModelGroupMapper implements ModelMapper<EntityResponse, MLModelGr
FORMS_ASPECT_NAME,
((entity, dataMap) ->
entity.setForms(FormsMapper.map(new Forms(dataMap), entityUrn.toString()))));
mappingHelper.mapToResult(
APPLICATION_MEMBERSHIP_ASPECT_NAME,
(mlModelGroup, dataMap) -> mapApplicationAssociation(context, mlModelGroup, dataMap));
if (context != null && !canView(context.getOperationContext(), entityUrn)) {
return AuthorizationUtils.restrictEntity(mappingHelper.getResult(), MLModelGroup.class);
@ -172,4 +177,13 @@ public class MLModelGroupMapper implements ModelMapper<EntityResponse, MLModelGr
}
entity.setEditableProperties(editableProperties);
}
private static void mapApplicationAssociation(
@Nullable final QueryContext context,
@Nonnull MLModelGroup mlModelGroup,
@Nonnull DataMap dataMap) {
final Applications applications = new Applications(dataMap);
mlModelGroup.setApplication(
ApplicationAssociationMapper.map(context, applications, mlModelGroup.getUrn()));
}
}

View File

@ -3,6 +3,7 @@ package com.linkedin.datahub.graphql.types.mlmodel.mappers;
import static com.linkedin.datahub.graphql.authorization.AuthorizationUtils.canView;
import static com.linkedin.metadata.Constants.*;
import com.linkedin.application.Applications;
import com.linkedin.common.BrowsePathsV2;
import com.linkedin.common.Cost;
import com.linkedin.common.DataPlatformInstance;
@ -24,6 +25,7 @@ import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.generated.FabricType;
import com.linkedin.datahub.graphql.generated.MLModel;
import com.linkedin.datahub.graphql.generated.MLModelEditableProperties;
import com.linkedin.datahub.graphql.types.application.ApplicationAssociationMapper;
import com.linkedin.datahub.graphql.types.common.mappers.BrowsePathsV2Mapper;
import com.linkedin.datahub.graphql.types.common.mappers.CostMapper;
import com.linkedin.datahub.graphql.types.common.mappers.DataPlatformInstanceAspectMapper;
@ -187,6 +189,9 @@ public class MLModelMapper implements ModelMapper<EntityResponse, MLModel> {
(entity, dataMap) ->
entity.setVersionProperties(
VersionPropertiesMapper.map(context, new VersionProperties(dataMap))));
mappingHelper.mapToResult(
APPLICATION_MEMBERSHIP_ASPECT_NAME,
(mlModel, dataMap) -> mapApplicationAssociation(context, mlModel, dataMap));
if (context != null && !canView(context.getOperationContext(), entityUrn)) {
return AuthorizationUtils.restrictEntity(mappingHelper.getResult(), MLModel.class);
@ -249,4 +254,11 @@ public class MLModelMapper implements ModelMapper<EntityResponse, MLModel> {
}
entity.setEditableProperties(editableProperties);
}
private static void mapApplicationAssociation(
@Nullable final QueryContext context, @Nonnull MLModel mlModel, @Nonnull DataMap dataMap) {
final Applications applications = new Applications(dataMap);
mlModel.setApplication(
ApplicationAssociationMapper.map(context, applications, mlModel.getUrn()));
}
}

View File

@ -357,6 +357,21 @@ type VisualConfig {
Configuration for custom theme-ing
"""
theme: ThemeConfig
"""
Configuration for the application sidebar section
"""
application: ApplicationConfig
}
"""
Configuration for the application sidebar section
"""
type ApplicationConfig {
"""
Whether to show the application sidebar section even when empty
"""
showSidebarSectionWhenEmpty: Boolean
}
"""

View File

@ -1689,6 +1689,11 @@ type Dataset implements EntityWithRelationships & Entity & BrowsableEntity {
"""
domain: DomainAssociation
"""
The application associated with the dataset
"""
application: ApplicationAssociation
"""
The forms associated with the Dataset
"""
@ -2136,6 +2141,11 @@ type VersionedDataset implements Entity {
"""
domain: DomainAssociation
"""
The application associated with the entity
"""
application: ApplicationAssociation
"""
Experimental! The resolved health status of the asset
"""
@ -2353,6 +2363,11 @@ type GlossaryTerm implements Entity {
"""
domain: DomainAssociation
"""
The application associated with the glossary term
"""
application: ApplicationAssociation
"""
References to internal resources related to the Glossary Term
"""
@ -2997,6 +3012,11 @@ type Container implements Entity {
"""
domain: DomainAssociation
"""
The application associated with the entity
"""
application: ApplicationAssociation
"""
The deprecation status of the container
"""
@ -5463,6 +5483,11 @@ type Notebook implements Entity & BrowsableEntity {
"""
domain: DomainAssociation
"""
The application associated with the entity
"""
application: ApplicationAssociation
"""
The specific instance of the data platform that this entity belongs to
"""
@ -5750,6 +5775,11 @@ type Dashboard implements EntityWithRelationships & Entity & BrowsableEntity {
"""
domain: DomainAssociation
"""
The application associated with the entity
"""
application: ApplicationAssociation
"""
The specific instance of the data platform that this entity belongs to
"""
@ -6088,6 +6118,11 @@ type Chart implements EntityWithRelationships & Entity & BrowsableEntity {
"""
domain: DomainAssociation
"""
The application associated with the entity
"""
application: ApplicationAssociation
"""
The specific instance of the data platform that this entity belongs to
"""
@ -6487,6 +6522,11 @@ type DataFlow implements EntityWithRelationships & Entity & BrowsableEntity {
"""
domain: DomainAssociation
"""
The application associated with the entity
"""
application: ApplicationAssociation
"""
The specific instance of the data platform that this entity belongs to
"""
@ -6739,6 +6779,11 @@ type DataJob implements EntityWithRelationships & Entity & BrowsableEntity {
"""
domain: DomainAssociation
"""
The application associated with the entity
"""
application: ApplicationAssociation
"""
Granular API for querying edges extending from this entity
"""
@ -10028,6 +10073,11 @@ type MLModel implements EntityWithRelationships & Entity & BrowsableEntity {
"""
domain: DomainAssociation
"""
The application associated with the entity
"""
application: ApplicationAssociation
"""
An additional set of of read write properties
"""
@ -10160,6 +10210,11 @@ type MLModelGroup implements EntityWithRelationships & Entity & BrowsableEntity
"""
domain: DomainAssociation
"""
The application associated with the entity
"""
application: ApplicationAssociation
"""
An additional set of of read write properties
"""
@ -10342,6 +10397,11 @@ type MLFeature implements EntityWithRelationships & Entity {
"""
domain: DomainAssociation
"""
The application associated with the entity
"""
application: ApplicationAssociation
"""
An additional set of of read write properties
"""
@ -10596,6 +10656,11 @@ type MLPrimaryKey implements EntityWithRelationships & Entity {
"""
domain: DomainAssociation
"""
The application associated with the entity
"""
application: ApplicationAssociation
"""
An additional set of of read write properties
"""
@ -10745,6 +10810,11 @@ type MLFeatureTable implements EntityWithRelationships & Entity & BrowsableEntit
"""
domain: DomainAssociation
"""
The application associated with the entity
"""
application: ApplicationAssociation
"""
An additional set of of read write properties
"""
@ -11104,6 +11174,18 @@ type SubTypes {
typeNames: [String!]
}
type ApplicationAssociation {
"""
The application related to the assocaited urn
"""
application: Application!
"""
Reference back to the tagged urn for tracking purposes e.g. when sibling nodes are merged together
"""
associatedUrn: String!
}
type DomainAssociation {
"""
The domain related to the assocaited urn
@ -12705,6 +12787,11 @@ type DataProduct implements Entity {
"""
domain: DomainAssociation
"""
The application associated with the data product
"""
application: ApplicationAssociation
"""
Tags used for searching Data Product
"""

View File

@ -333,6 +333,7 @@ export const dataset1 = {
},
],
domain: null,
application: null,
container: null,
health: [],
assertions: null,
@ -429,6 +430,7 @@ export const dataset2 = {
},
],
domain: null,
application: null,
container: null,
health: [],
assertions: null,
@ -670,6 +672,7 @@ export const dataset3 = {
},
],
domain: null,
application: null,
container: null,
lineage: null,
relationships: null,
@ -1410,6 +1413,7 @@ export const dataFlow1 = {
},
},
domain: null,
application: null,
deprecation: null,
autoRenderAspects: [],
activeIncidents: null,
@ -1497,6 +1501,7 @@ export const dataJob1 = {
],
},
domain: null,
application: null,
status: null,
deprecation: null,
autoRenderAspects: [],
@ -1663,6 +1668,7 @@ export const dataJob2 = {
],
},
domain: null,
application: null,
upstream: null,
downstream: null,
deprecation: null,
@ -1736,6 +1742,7 @@ export const dataJob3 = {
],
},
domain: null,
application: null,
upstream: null,
downstream: null,
status: null,

View File

@ -100,6 +100,10 @@ export enum EntityCapabilityType {
* Assigning Business Attribute to a entity
*/
BUSINESS_ATTRIBUTES,
/**
* Assigning an application to a entity
*/
APPLICATIONS,
}
/**

View File

@ -24,6 +24,10 @@ export enum EntityActionItem {
* Batch add a Data Product to a set of assets
*/
BATCH_ADD_DATA_PRODUCT,
/**
* Batch add an Application to a set of assets
*/
BATCH_ADD_APPLICATION,
}
interface Props {

View File

@ -4,6 +4,7 @@ import React from 'react';
import { FetchedEntity } from '@app/lineage/types';
import {
ApplicationAssociation,
BrowsePathV2,
Container,
CustomPropertiesEntry,
@ -91,6 +92,7 @@ export type GenericEntityProperties = {
glossaryTerms?: Maybe<GlossaryTerms>;
ownership?: Maybe<Ownership>;
domain?: Maybe<DomainAssociation>;
application?: Maybe<ApplicationAssociation>;
dataProduct?: Maybe<EntityRelationshipsResult>;
platform?: Maybe<DataPlatform>;
dataPlatformInstance?: Maybe<DataPlatformInstance>;

View File

@ -96,6 +96,10 @@ export enum EntityCapabilityType {
* Lineage information of an entity
*/
LINEAGE,
/**
* Assigning the entity to an application
*/
APPLICATIONS,
}
export interface EntityMenuActions {

View File

@ -24,6 +24,7 @@ import { SidebarGlossaryTermsSection } from '@app/entityV2/shared/containers/pro
import { SidebarTagsSection } from '@app/entityV2/shared/containers/profile/sidebar/SidebarTagsSection';
import StatusSection from '@app/entityV2/shared/containers/profile/sidebar/shared/StatusSection';
import { getDataForEntityType } from '@app/entityV2/shared/containers/profile/utils';
import { EntityActionItem } from '@app/entityV2/shared/entity/EntityActions';
import SidebarNotesSection from '@app/entityV2/shared/sidebarSection/SidebarNotesSection';
import SidebarStructuredProperties from '@app/entityV2/shared/sidebarSection/SidebarStructuredProperties';
import { DocumentationTab } from '@app/entityV2/shared/tabs/Documentation/DocumentationTab';
@ -98,7 +99,7 @@ export class ApplicationEntity implements Entity<Application> {
useEntityQuery={useGetApplicationQuery}
useUpdateQuery={undefined}
getOverrideProperties={this.getOverridePropertiesFromEntity}
headerActionItems={new Set([])}
headerActionItems={new Set([EntityActionItem.BATCH_ADD_APPLICATION])}
headerDropdownItems={headerDropdownItems}
isNameEditable
tabs={[

View File

@ -20,6 +20,7 @@ import { EntityMenuItems } from '@app/entityV2/shared/EntityDropdown/EntityMenuA
import { SubType, TYPE_ICON_CLASS_NAME } from '@app/entityV2/shared/components/subtypes';
import { EntityProfile } from '@app/entityV2/shared/containers/profile/EntityProfile';
import { SidebarAboutSection } from '@app/entityV2/shared/containers/profile/sidebar/AboutSection/SidebarAboutSection';
import { SidebarApplicationSection } from '@app/entityV2/shared/containers/profile/sidebar/Applications/SidebarApplicationSection';
import SidebarChartHeaderSection from '@app/entityV2/shared/containers/profile/sidebar/Chart/Header/SidebarChartHeaderSection';
import DataProductSection from '@app/entityV2/shared/containers/profile/sidebar/DataProduct/DataProductSection';
import { SidebarDomainSection } from '@app/entityV2/shared/containers/profile/sidebar/Domain/SidebarDomainSection';
@ -231,6 +232,9 @@ export class ChartEntity implements Entity<Chart> {
{
component: SidebarDomainSection,
},
{
component: SidebarApplicationSection,
},
{
component: DataProductSection,
},
@ -386,6 +390,7 @@ export class ChartEntity implements Entity<Chart> {
EntityCapabilityType.TEST,
EntityCapabilityType.LINEAGE,
EntityCapabilityType.HEALTH,
EntityCapabilityType.APPLICATIONS,
]);
};

View File

@ -21,6 +21,7 @@ import { EntityMenuItems } from '@app/entityV2/shared/EntityDropdown/EntityMenuA
import { TYPE_ICON_CLASS_NAME } from '@app/entityV2/shared/components/subtypes';
import { EntityProfile } from '@app/entityV2/shared/containers/profile/EntityProfile';
import { SidebarAboutSection } from '@app/entityV2/shared/containers/profile/sidebar/AboutSection/SidebarAboutSection';
import { SidebarApplicationSection } from '@app/entityV2/shared/containers/profile/sidebar/Applications/SidebarApplicationSection';
import SidebarDashboardHeaderSection from '@app/entityV2/shared/containers/profile/sidebar/Dashboard/Header/SidebarDashboardHeaderSection';
import DataProductSection from '@app/entityV2/shared/containers/profile/sidebar/DataProduct/DataProductSection';
import { SidebarDomainSection } from '@app/entityV2/shared/containers/profile/sidebar/Domain/SidebarDomainSection';
@ -225,6 +226,9 @@ export class DashboardEntity implements Entity<Dashboard> {
{
component: SidebarDomainSection,
},
{
component: SidebarApplicationSection,
},
{
component: DataProductSection,
},
@ -393,6 +397,7 @@ export class DashboardEntity implements Entity<Dashboard> {
EntityCapabilityType.TEST,
EntityCapabilityType.LINEAGE,
EntityCapabilityType.HEALTH,
EntityCapabilityType.APPLICATIONS,
]);
};

View File

@ -9,6 +9,7 @@ import { EntityMenuItems } from '@app/entityV2/shared/EntityDropdown/EntityMenuA
import { TYPE_ICON_CLASS_NAME } from '@app/entityV2/shared/components/subtypes';
import { EntityProfile } from '@app/entityV2/shared/containers/profile/EntityProfile';
import { SidebarAboutSection } from '@app/entityV2/shared/containers/profile/sidebar/AboutSection/SidebarAboutSection';
import { SidebarApplicationSection } from '@app/entityV2/shared/containers/profile/sidebar/Applications/SidebarApplicationSection';
import DataProductSection from '@app/entityV2/shared/containers/profile/sidebar/DataProduct/DataProductSection';
import { SidebarDomainSection } from '@app/entityV2/shared/containers/profile/sidebar/Domain/SidebarDomainSection';
import { SidebarOwnerSection } from '@app/entityV2/shared/containers/profile/sidebar/Ownership/sidebar/SidebarOwnerSection';
@ -141,6 +142,9 @@ export class DataFlowEntity implements Entity<DataFlow> {
{
component: SidebarDomainSection,
},
{
component: SidebarApplicationSection,
},
{
component: DataProductSection,
},
@ -257,6 +261,7 @@ export class DataFlowEntity implements Entity<DataFlow> {
EntityCapabilityType.TEST,
EntityCapabilityType.LINEAGE,
EntityCapabilityType.HEALTH,
EntityCapabilityType.APPLICATIONS,
]);
};
}

View File

@ -11,6 +11,7 @@ import { EntityMenuItems } from '@app/entityV2/shared/EntityDropdown/EntityMenuA
import { TYPE_ICON_CLASS_NAME } from '@app/entityV2/shared/components/subtypes';
import { EntityProfile } from '@app/entityV2/shared/containers/profile/EntityProfile';
import { SidebarAboutSection } from '@app/entityV2/shared/containers/profile/sidebar/AboutSection/SidebarAboutSection';
import { SidebarApplicationSection } from '@app/entityV2/shared/containers/profile/sidebar/Applications/SidebarApplicationSection';
import DataProductSection from '@app/entityV2/shared/containers/profile/sidebar/DataProduct/DataProductSection';
import { SidebarDomainSection } from '@app/entityV2/shared/containers/profile/sidebar/Domain/SidebarDomainSection';
import SidebarLineageSection from '@app/entityV2/shared/containers/profile/sidebar/Lineage/SidebarLineageSection';
@ -50,7 +51,6 @@ const headerDropdownItems = new Set([
EntityMenuItems.SHARE,
EntityMenuItems.UPDATE_DEPRECATION,
EntityMenuItems.ANNOUNCE,
EntityMenuItems.EXTERNAL_URL,
]);
/**
@ -162,6 +162,7 @@ export class DataJobEntity implements Entity<DataJob> {
{ component: SidebarDataJobTransformationLogicSection },
{ component: SidebarOwnerSection },
{ component: SidebarDomainSection },
{ component: SidebarApplicationSection },
{ component: DataProductSection },
{ component: SidebarGlossaryTermsSection },
{ component: SidebarTagsSection },
@ -323,6 +324,7 @@ export class DataJobEntity implements Entity<DataJob> {
EntityCapabilityType.TEST,
EntityCapabilityType.LINEAGE,
EntityCapabilityType.HEALTH,
EntityCapabilityType.APPLICATIONS,
]);
};
}

View File

@ -17,6 +17,7 @@ import { TYPE_ICON_CLASS_NAME } from '@app/entityV2/shared/components/subtypes';
import { EntityProfileTab } from '@app/entityV2/shared/constants';
import { EntityProfile } from '@app/entityV2/shared/containers/profile/EntityProfile';
import { SidebarAboutSection } from '@app/entityV2/shared/containers/profile/sidebar/AboutSection/SidebarAboutSection';
import { SidebarApplicationSection } from '@app/entityV2/shared/containers/profile/sidebar/Applications/SidebarApplicationSection';
import { SidebarViewDefinitionSection } from '@app/entityV2/shared/containers/profile/sidebar/Dataset/View/SidebarViewDefinitionSection';
import { SidebarDomainSection } from '@app/entityV2/shared/containers/profile/sidebar/Domain/SidebarDomainSection';
import { SidebarOwnerSection } from '@app/entityV2/shared/containers/profile/sidebar/Ownership/sidebar/SidebarOwnerSection';
@ -154,6 +155,9 @@ export class DataProductEntity implements Entity<DataProduct> {
updateOnly: true,
},
},
{
component: SidebarApplicationSection,
},
// TODO: Is someone actually using the below code?
{
component: SidebarViewDefinitionSection,
@ -262,6 +266,7 @@ export class DataProductEntity implements Entity<DataProduct> {
EntityCapabilityType.GLOSSARY_TERMS,
EntityCapabilityType.TAGS,
EntityCapabilityType.DOMAINS,
EntityCapabilityType.APPLICATIONS,
]);
};

View File

@ -24,6 +24,7 @@ import { EntityMenuItems } from '@app/entityV2/shared/EntityDropdown/EntityMenuA
import { SubType, TYPE_ICON_CLASS_NAME } from '@app/entityV2/shared/components/subtypes';
import { EntityProfile } from '@app/entityV2/shared/containers/profile/EntityProfile';
import { SidebarAboutSection } from '@app/entityV2/shared/containers/profile/sidebar/AboutSection/SidebarAboutSection';
import { SidebarApplicationSection } from '@app/entityV2/shared/containers/profile/sidebar/Applications/SidebarApplicationSection';
import DataProductSection from '@app/entityV2/shared/containers/profile/sidebar/DataProduct/DataProductSection';
import SidebarDatasetHeaderSection from '@app/entityV2/shared/containers/profile/sidebar/Dataset/Header/SidebarDatasetHeaderSection';
import { SidebarDomainSection } from '@app/entityV2/shared/containers/profile/sidebar/Domain/SidebarDomainSection';
@ -188,7 +189,6 @@ export class DatasetEntity implements Entity<Dataset> {
name: 'Lineage',
component: LineageTab,
icon: PartitionOutlined,
supportsFullsize: true,
},
{
name: 'Access',
@ -287,6 +287,7 @@ export class DatasetEntity implements Entity<Dataset> {
{ component: SidebarLineageSection },
{ component: SidebarOwnerSection },
{ component: SidebarDomainSection },
{ component: SidebarApplicationSection },
{ component: DataProductSection },
{ component: SidebarTagsSection },
{ component: SidebarGlossaryTermsSection },
@ -514,6 +515,7 @@ export class DatasetEntity implements Entity<Dataset> {
EntityCapabilityType.TEST,
EntityCapabilityType.LINEAGE,
EntityCapabilityType.HEALTH,
EntityCapabilityType.APPLICATIONS,
]);
};

View File

@ -9,6 +9,7 @@ import { EntityMenuItems } from '@app/entityV2/shared/EntityDropdown/EntityMenuA
import { TYPE_ICON_CLASS_NAME } from '@app/entityV2/shared/components/subtypes';
import { EntityProfile } from '@app/entityV2/shared/containers/profile/EntityProfile';
import { SidebarAboutSection } from '@app/entityV2/shared/containers/profile/sidebar/AboutSection/SidebarAboutSection';
import { SidebarApplicationSection } from '@app/entityV2/shared/containers/profile/sidebar/Applications/SidebarApplicationSection';
import { SidebarOwnerSection } from '@app/entityV2/shared/containers/profile/sidebar/Ownership/sidebar/SidebarOwnerSection';
import StatusSection from '@app/entityV2/shared/containers/profile/sidebar/shared/StatusSection';
import { getDataForEntityType } from '@app/entityV2/shared/containers/profile/utils';
@ -133,6 +134,9 @@ class GlossaryNodeEntity implements Entity<GlossaryNode> {
{
component: SidebarOwnerSection,
},
{
component: SidebarApplicationSection,
},
{
component: SidebarStructuredProperties,
},
@ -197,6 +201,7 @@ class GlossaryNodeEntity implements Entity<GlossaryNode> {
EntityCapabilityType.OWNERS,
EntityCapabilityType.DEPRECATION,
EntityCapabilityType.SOFT_DELETE,
EntityCapabilityType.APPLICATIONS,
]);
};

View File

@ -13,6 +13,7 @@ import { EntityMenuItems } from '@app/entityV2/shared/EntityDropdown/EntityMenuA
import { TYPE_ICON_CLASS_NAME } from '@app/entityV2/shared/components/subtypes';
import { EntityProfile } from '@app/entityV2/shared/containers/profile/EntityProfile';
import { SidebarAboutSection } from '@app/entityV2/shared/containers/profile/sidebar/AboutSection/SidebarAboutSection';
import { SidebarApplicationSection } from '@app/entityV2/shared/containers/profile/sidebar/Applications/SidebarApplicationSection';
import { SidebarDomainSection } from '@app/entityV2/shared/containers/profile/sidebar/Domain/SidebarDomainSection';
import { SidebarOwnerSection } from '@app/entityV2/shared/containers/profile/sidebar/Ownership/sidebar/SidebarOwnerSection';
import SidebarEntityHeader from '@app/entityV2/shared/containers/profile/sidebar/SidebarEntityHeader';
@ -172,6 +173,9 @@ export class GlossaryTermEntity implements Entity<GlossaryTerm> {
hideOwnerType: true,
},
},
{
component: SidebarApplicationSection,
},
{
component: SidebarStructuredProperties,
},
@ -239,6 +243,7 @@ export class GlossaryTermEntity implements Entity<GlossaryTerm> {
EntityCapabilityType.OWNERS,
EntityCapabilityType.DEPRECATION,
EntityCapabilityType.SOFT_DELETE,
EntityCapabilityType.APPLICATIONS,
]);
};

View File

@ -9,6 +9,7 @@ import { EntityMenuItems } from '@app/entityV2/shared/EntityDropdown/EntityMenuA
import { TYPE_ICON_CLASS_NAME } from '@app/entityV2/shared/components/subtypes';
import { EntityProfile } from '@app/entityV2/shared/containers/profile/EntityProfile';
import { SidebarAboutSection } from '@app/entityV2/shared/containers/profile/sidebar/AboutSection/SidebarAboutSection';
import { SidebarApplicationSection } from '@app/entityV2/shared/containers/profile/sidebar/Applications/SidebarApplicationSection';
import DataProductSection from '@app/entityV2/shared/containers/profile/sidebar/DataProduct/DataProductSection';
import { SidebarDomainSection } from '@app/entityV2/shared/containers/profile/sidebar/Domain/SidebarDomainSection';
import { SidebarOwnerSection } from '@app/entityV2/shared/containers/profile/sidebar/Ownership/sidebar/SidebarOwnerSection';
@ -154,6 +155,9 @@ export class MLFeatureEntity implements Entity<MlFeature> {
{
component: SidebarDomainSection,
},
{
component: SidebarApplicationSection,
},
{
component: DataProductSection,
},
@ -269,6 +273,7 @@ export class MLFeatureEntity implements Entity<MlFeature> {
EntityCapabilityType.SOFT_DELETE,
EntityCapabilityType.DATA_PRODUCTS,
EntityCapabilityType.LINEAGE,
EntityCapabilityType.APPLICATIONS,
]);
};
}

View File

@ -10,6 +10,7 @@ import { EntityMenuItems } from '@app/entityV2/shared/EntityDropdown/EntityMenuA
import { TYPE_ICON_CLASS_NAME } from '@app/entityV2/shared/components/subtypes';
import { EntityProfile } from '@app/entityV2/shared/containers/profile/EntityProfile';
import { SidebarAboutSection } from '@app/entityV2/shared/containers/profile/sidebar/AboutSection/SidebarAboutSection';
import { SidebarApplicationSection } from '@app/entityV2/shared/containers/profile/sidebar/Applications/SidebarApplicationSection';
import DataProductSection from '@app/entityV2/shared/containers/profile/sidebar/DataProduct/DataProductSection';
import { SidebarDomainSection } from '@app/entityV2/shared/containers/profile/sidebar/Domain/SidebarDomainSection';
import { SidebarOwnerSection } from '@app/entityV2/shared/containers/profile/sidebar/Ownership/sidebar/SidebarOwnerSection';
@ -136,6 +137,9 @@ export class MLFeatureTableEntity implements Entity<MlFeatureTable> {
{
component: SidebarDomainSection,
},
{
component: SidebarApplicationSection,
},
{
component: DataProductSection,
},
@ -235,6 +239,7 @@ export class MLFeatureTableEntity implements Entity<MlFeatureTable> {
EntityCapabilityType.SOFT_DELETE,
EntityCapabilityType.DATA_PRODUCTS,
EntityCapabilityType.LINEAGE,
EntityCapabilityType.APPLICATIONS,
]);
};
}

View File

@ -12,6 +12,7 @@ import { EntityMenuItems } from '@app/entityV2/shared/EntityDropdown/EntityMenuA
import { TYPE_ICON_CLASS_NAME } from '@app/entityV2/shared/components/subtypes';
import { EntityProfile } from '@app/entityV2/shared/containers/profile/EntityProfile';
import { SidebarAboutSection } from '@app/entityV2/shared/containers/profile/sidebar/AboutSection/SidebarAboutSection';
import { SidebarApplicationSection } from '@app/entityV2/shared/containers/profile/sidebar/Applications/SidebarApplicationSection';
import DataProductSection from '@app/entityV2/shared/containers/profile/sidebar/DataProduct/DataProductSection';
import { SidebarDomainSection } from '@app/entityV2/shared/containers/profile/sidebar/Domain/SidebarDomainSection';
import { SidebarOwnerSection } from '@app/entityV2/shared/containers/profile/sidebar/Ownership/sidebar/SidebarOwnerSection';
@ -161,6 +162,9 @@ export class MLModelEntity implements Entity<MlModel> {
{
component: SidebarDomainSection,
},
{
component: SidebarApplicationSection,
},
{
component: DataProductSection,
},
@ -257,6 +261,7 @@ export class MLModelEntity implements Entity<MlModel> {
EntityCapabilityType.SOFT_DELETE,
EntityCapabilityType.DATA_PRODUCTS,
EntityCapabilityType.LINEAGE,
EntityCapabilityType.APPLICATIONS,
]);
};
}

View File

@ -10,6 +10,7 @@ import { EntityMenuItems } from '@app/entityV2/shared/EntityDropdown/EntityMenuA
import { TYPE_ICON_CLASS_NAME } from '@app/entityV2/shared/components/subtypes';
import { EntityProfile } from '@app/entityV2/shared/containers/profile/EntityProfile';
import { SidebarAboutSection } from '@app/entityV2/shared/containers/profile/sidebar/AboutSection/SidebarAboutSection';
import { SidebarApplicationSection } from '@app/entityV2/shared/containers/profile/sidebar/Applications/SidebarApplicationSection';
import DataProductSection from '@app/entityV2/shared/containers/profile/sidebar/DataProduct/DataProductSection';
import { SidebarDomainSection } from '@app/entityV2/shared/containers/profile/sidebar/Domain/SidebarDomainSection';
import { SidebarOwnerSection } from '@app/entityV2/shared/containers/profile/sidebar/Ownership/sidebar/SidebarOwnerSection';
@ -136,6 +137,9 @@ export class MLModelGroupEntity implements Entity<MlModelGroup> {
{
component: SidebarDomainSection,
},
{
component: SidebarApplicationSection,
},
{
component: DataProductSection,
},
@ -232,6 +236,7 @@ export class MLModelGroupEntity implements Entity<MlModelGroup> {
EntityCapabilityType.SOFT_DELETE,
EntityCapabilityType.DATA_PRODUCTS,
EntityCapabilityType.LINEAGE,
EntityCapabilityType.APPLICATIONS,
]);
};
}

View File

@ -172,6 +172,10 @@ export const EMPTY_MESSAGES = {
title: 'No product yet',
description: 'Group related entities based on shared characteristics by adding them to a Data Product.',
},
application: {
title: 'No application yet',
description: 'Associate entities with applications to track ownership and lifecycle.',
},
contains: {
title: 'Does not Contain any Glossary Terms',
description: 'Terms can contain other terms to represent a "Has A" style relationship.',

View File

@ -0,0 +1,79 @@
import { Modal, Select, Typography, message } from 'antd';
import React, { useState } from 'react';
import { useBatchSetApplicationMutation, useGetApplicationsListQuery } from '@graphql/application.generated';
import { Application, EntityType } from '@types';
interface Props {
urns: string[];
onCloseModal: () => void;
refetch?: () => void;
}
export const SetApplicationModal = ({ urns, onCloseModal, refetch }: Props) => {
const [applicationUrn, setApplicationUrn] = useState<string | undefined>(undefined);
const { data, loading, error } = useGetApplicationsListQuery({
variables: {
input: {
start: 0,
count: 1000,
query: '',
types: [EntityType.Application],
},
},
});
const [batchSetApplicationMutation] = useBatchSetApplicationMutation();
const onOk = () => {
if (!applicationUrn) {
return;
}
batchSetApplicationMutation({
variables: {
input: {
applicationUrn,
resourceUrns: urns,
},
},
})
.then(() => {
message.success({ content: 'Application set', duration: 2 });
refetch?.();
})
.catch((e: unknown) => {
message.destroy();
if (e instanceof Error) {
message.error({ content: `Failed to set application: \n ${e.message || ''}`, duration: 3 });
}
})
.finally(() => {
onCloseModal();
});
};
const applicationOptions =
data?.searchAcrossEntities?.searchResults
?.map((r) => r.entity)
.filter((entity): entity is Application => entity.__typename === 'Application')
.map((appEntity) => {
return {
value: appEntity.urn,
label: appEntity.properties?.name || '',
};
}) || [];
return (
<Modal title="Set Application" open onOk={onOk} onCancel={onCloseModal} closable>
<Select
showSearch
style={{ width: '100%' }}
placeholder="Select an application"
onChange={(value) => setApplicationUrn(value)}
filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
options={applicationOptions}
loading={loading}
/>
{error && <Typography.Text type="danger">Failed to load applications: {error.message}</Typography.Text>}
</Modal>
);
};

View File

@ -0,0 +1,145 @@
import AddRoundedIcon from '@mui/icons-material/AddRounded';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import { Modal, message } from 'antd';
import React, { useState } from 'react';
import styled from 'styled-components';
import { useEntityData, useMutationUrn, useRefetch } from '@app/entity/shared/EntityContext';
import { EMPTY_MESSAGES } from '@app/entityV2/shared/constants';
import { SetApplicationModal } from '@app/entityV2/shared/containers/profile/sidebar/Applications/SetApplicationModal';
import EmptySectionText from '@app/entityV2/shared/containers/profile/sidebar/EmptySectionText';
import SectionActionButton from '@app/entityV2/shared/containers/profile/sidebar/SectionActionButton';
import { SidebarSection } from '@app/entityV2/shared/containers/profile/sidebar/SidebarSection';
import { ApplicationLink } from '@app/shared/tags/ApplicationLink';
import { useAppConfig } from '@app/useAppConfig';
import { useBatchSetApplicationMutation } from '@graphql/application.generated';
const Content = styled.div`
display: flex;
align-items: start;
justify-content: start;
flex-wrap: wrap;
text-wrap: wrap;
`;
const ApplicationLinkWrapper = styled.div`
margin-right: 12px;
display: flex;
align-items: center;
`;
interface PropertiesProps {
updateOnly?: boolean;
}
interface Props {
readOnly?: boolean;
properties?: PropertiesProps;
}
export const SidebarApplicationSection = ({ readOnly, properties }: Props) => {
const {
config: { visualConfig },
} = useAppConfig();
const updateOnly = properties?.updateOnly;
const { entityData } = useEntityData();
const refetch = useRefetch();
const urn = useMutationUrn();
const [batchSetApplicationMutation] = useBatchSetApplicationMutation();
const [showModal, setShowModal] = useState(false);
const application = entityData?.application?.application;
const canEditApplication = !!entityData?.privileges?.canEditProperties;
console.log('application', application);
if (!application && !visualConfig.application?.showSidebarSectionWhenEmpty) {
return null;
}
const removeApplication = () => {
batchSetApplicationMutation({
variables: {
input: {
applicationUrn: null,
resourceUrns: [urn],
},
},
})
.then(() => {
message.success({ content: 'Removed Application.', duration: 2 });
refetch?.();
})
.catch((e: unknown) => {
message.destroy();
if (e instanceof Error) {
message.error({ content: `Failed to remove application: \n ${e.message || ''}`, duration: 3 });
}
});
};
const onRemoveApplication = () => {
Modal.confirm({
title: `Confirm Application Removal`,
content: `Are you sure you want to remove this application?`,
onOk() {
removeApplication();
},
onCancel() {},
okText: 'Yes',
maskClosable: true,
closable: true,
});
};
return (
<div className="sidebar-application-section">
<SidebarSection
title="Application"
content={
<Content>
{application && (
<ApplicationLinkWrapper>
<ApplicationLink
application={application}
closable={!readOnly && !updateOnly && canEditApplication}
readOnly={readOnly}
onClose={(e) => {
e.preventDefault();
onRemoveApplication();
}}
fontSize={12}
/>
</ApplicationLinkWrapper>
)}
{(!application || !!updateOnly) && (
<>{!application && <EmptySectionText message={EMPTY_MESSAGES.application.title} />}</>
)}
</Content>
}
extra={
!readOnly && (
<SectionActionButton
dataTestId="add-applications-button"
button={application ? <EditOutlinedIcon /> : <AddRoundedIcon />}
onClick={(event) => {
setShowModal(true);
event.stopPropagation();
}}
actionPrivilege={canEditApplication}
/>
)
}
/>
{showModal && (
<SetApplicationModal
urns={[urn]}
refetch={refetch}
onCloseModal={() => {
setShowModal(false);
}}
/>
)}
</div>
);
};

View File

@ -11,6 +11,7 @@ import { SearchSelectModal } from '@app/entityV2/shared/components/styled/search
import { handleBatchError } from '@app/entityV2/shared/utils';
import { useEntityRegistry } from '@app/useEntityRegistry';
import { useBatchSetApplicationMutation } from '@graphql/application.generated';
import { useBatchSetDataProductMutation } from '@graphql/dataProduct.generated';
import { useBatchAddTermsMutation, useBatchSetDomainMutation } from '@graphql/mutations.generated';
import { EntityType } from '@types';
@ -36,6 +37,10 @@ export enum EntityActionItem {
* Add a new Glossary Node as child
*/
ADD_CHILD_GLOSSARY_NODE,
/**
* Batch add an Application to a set of assets
*/
BATCH_ADD_APPLICATION,
}
const ButtonWrapper = styled.div`
@ -72,9 +77,11 @@ function EntityActions(props: Props) {
const [isBatchSetDataProductModalVisible, setIsBatchSetDataProductModalVisible] = useState(false);
const [isCreateTermModalVisible, setIsCreateTermModalVisible] = useState(false);
const [isCreateNodeModalVisible, setIsCreateNodeModalVisible] = useState(false);
const [isBatchSetApplicationModalVisible, setIsBatchSetApplicationModalVisible] = useState(false);
const [batchAddTermsMutation] = useBatchAddTermsMutation();
const [batchSetDomainMutation] = useBatchSetDomainMutation();
const [batchSetDataProductMutation] = useBatchSetDataProductMutation();
const [batchSetApplicationMutation] = useBatchSetApplicationMutation();
// eslint-disable-next-line
const batchAddGlossaryTerms = (entityUrns: Array<string>) => {
@ -186,6 +193,40 @@ function EntityActions(props: Props) {
});
};
const batchSetApplication = (entityUrns: Array<string>) => {
batchSetApplicationMutation({
variables: {
input: {
applicationUrn: urn,
resourceUrns: entityUrns,
},
},
})
.then(({ errors }) => {
if (!errors) {
setIsBatchSetApplicationModalVisible(false);
message.loading({ content: 'Updating...', duration: 3 });
setTimeout(() => {
message.success({
content: `Added assets to Application!`,
duration: 3,
});
refetchForEntity?.();
setShouldRefetchEmbeddedListSearch?.(true);
}, 3000);
}
})
.catch((e) => {
message.destroy();
message.error(
handleBatchError(entityUrns, e, {
content: `Failed to add assets to Application. An unknown error occurred.`,
duration: 3,
}),
);
});
};
const { entityData } = useEntityData();
const canCreateGlossaryEntity = !!entityData?.privileges?.canManageChildren;
@ -245,6 +286,13 @@ function EntityActions(props: Props) {
</Button>
</Tooltip>
)}
{actionItems.has(EntityActionItem.BATCH_ADD_APPLICATION) && (
<Tooltip title="Add Assets to Application" showArrow={false} placement="bottom">
<Button variant="outline" onClick={() => setIsBatchSetApplicationModalVisible(true)}>
<LinkOutlined /> Add to Assets
</Button>
</Tooltip>
)}
</ButtonWrapper>
{isBatchAddGlossaryTermModalVisible && (
<SearchSelectModal
@ -268,6 +316,17 @@ function EntityActions(props: Props) {
)}
/>
)}
{isBatchSetApplicationModalVisible && (
<SearchSelectModal
titleText="Add assets to Application"
continueText="Add"
onContinue={batchSetApplication}
onCancel={() => setIsBatchSetApplicationModalVisible(false)}
fixedEntityTypes={Array.from(
entityRegistry.getTypesWithSupportedCapabilities(EntityCapabilityType.APPLICATIONS),
)}
/>
)}
{isBatchSetDataProductModalVisible && (
<SearchSelectModal
titleText="Add assets to Data Product"

View File

@ -0,0 +1,36 @@
import { Tag } from 'antd';
import React from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { useEntityRegistry } from '@app/useEntityRegistry';
import { Application } from '@types';
const StyledTag = styled(Tag)`
&& {
margin: 2px;
}
`;
interface Props {
application: Application;
closable?: boolean;
onClose?: (e) => void;
readOnly?: boolean;
fontSize?: number;
}
export const ApplicationLink = ({ application, closable, onClose, readOnly, fontSize }: Props) => {
const entityRegistry = useEntityRegistry();
const applicationPath = entityRegistry.getPathName(application.type);
const applicationUrl = `/${applicationPath}/${encodeURIComponent(application.urn)}`;
return (
<Link to={applicationUrl}>
<StyledTag closable={!readOnly && closable} onClose={onClose} style={{ fontSize }}>
{application.properties?.name || application.urn}
</StyledTag>
</Link>
);
};

View File

@ -52,6 +52,9 @@ query appConfig {
theme {
themeId
}
application {
showSidebarSectionWhenEmpty
}
}
telemetryConfig {
enableThirdPartyLogging

View File

@ -49,6 +49,9 @@ query getChart($urn: String!) {
domain {
...entityDomain
}
application {
...entityApplication
}
...entityDataProduct
deprecation {
...deprecationFields

View File

@ -448,7 +448,9 @@ fragment nonRecursiveDatasetFields on Dataset {
domain {
...entityDomain
}
...entityDataProduct
application {
...entityApplication
}
container {
...entityContainer
}
@ -491,6 +493,9 @@ fragment nonRecursiveDataFlowFields on DataFlow {
domain {
...entityDomain
}
application {
...entityApplication
}
...entityDataProduct
deprecation {
...deprecationFields
@ -517,6 +522,9 @@ fragment nonRecursiveDataJobFields on DataJob {
domain {
...entityDomain
}
application {
...entityApplication
}
...entityDataProduct
deprecation {
...deprecationFields
@ -562,6 +570,9 @@ fragment dataJobFields on DataJob {
domain {
...entityDomain
}
application {
...entityApplication
}
...entityDataProduct
deprecation {
...deprecationFields
@ -640,6 +651,9 @@ fragment dashboardFields on Dashboard {
domain {
...entityDomain
}
application {
...entityApplication
}
...entityDataProduct
parentContainers {
...parentContainersFields
@ -736,6 +750,9 @@ fragment nonRecursiveMLFeature on MLFeature {
domain {
...entityDomain
}
application {
...entityApplication
}
...entityDataProduct
tags {
...globalTagsFields
@ -887,6 +904,9 @@ fragment nonRecursiveMLFeatureTable on MLFeatureTable {
domain {
...entityDomain
}
application {
...entityApplication
}
...entityDataProduct
tags {
...globalTagsFields
@ -1079,6 +1099,9 @@ fragment nonRecursiveMLModel on MLModel {
domain {
...entityDomain
}
application {
...entityApplication
}
...entityDataProduct
tags {
...globalTagsFields
@ -1123,6 +1146,9 @@ fragment nonRecursiveMLModelGroupFields on MLModelGroup {
domain {
...entityDomain
}
application {
...entityApplication
}
...entityDataProduct
tags {
...globalTagsFields
@ -1266,6 +1292,33 @@ fragment entityDomain on DomainAssociation {
associatedUrn
}
fragment entityApplication on ApplicationAssociation {
application {
urn
type
properties {
name
description
externalUrl
}
ownership {
...ownershipFields
}
tags {
...globalTagsFields
}
glossaryTerms {
...glossaryTerms
}
domain {
...entityDomain
}
children: relationships(input: { types: ["AssociatedWith"], direction: INCOMING, start: 0, count: 0 }) {
total
}
}
}
fragment entityDataProduct on Entity {
dataProduct: relationships(input: { types: ["DataProductContains"], direction: INCOMING, start: 0, count: 1 }) {
relationships {

View File

@ -0,0 +1,12 @@
package com.linkedin.metadata.config;
import lombok.Data;
@Data
public class ApplicationConfig {
/**
* Whether to show the application sidebar section even when empty - will add noise to the UI for
* teams that don't use applications.
*/
public boolean showSidebarSectionWhenEmpty;
}

View File

@ -31,4 +31,6 @@ public class VisualConfiguration {
/** Boolean flag enabled shows the full title of an entity in lineage view by default */
public boolean showFullTitleInLineage;
public ApplicationConfig application;
}

View File

@ -189,6 +189,8 @@ visualConfig:
entityProfile:
# we only support default tab for domains right now. In order to implement for other entities, update React code
domainDefaultTab: ${DOMAIN_DEFAULT_TAB:} # set to DOCUMENTATION_TAB to show documentation tab first
application:
showSidebarSectionWhenEmpty: ${APPLICATION_SHOW_SIDEBAR_SECTION_WHEN_EMPTY:false}
searchResult:
enableNameHighlight: ${SEARCH_RESULT_NAME_HIGHLIGHT_ENABLED:true} # Enables visual highlighting on search result names/descriptions.

View File

@ -1,9 +1,26 @@
import { aliasQuery, hasOperationName } from "../utils";
describe("applications", () => {
beforeEach(() => {
cy.setIsThemeV2Enabled(true);
Cypress.on("uncaught:exception", (err, runnable) => false);
cy.intercept("POST", "/api/v2/graphql", (req) => {
aliasQuery(req, "appConfig");
});
});
const setApplicationFeatureFlag = (isOn) => {
cy.intercept("POST", "/api/v2/graphql", (req) => {
if (hasOperationName(req, "appConfig")) {
req.alias = "gqlappConfigQuery";
req.on("response", (res) => {
res.body.data.appConfig.visualConfig.application.showSidebarSectionWhenEmpty =
isOn;
});
}
});
};
it("can see elements inside the application", () => {
cy.login();
cy.goToApplication(
@ -12,4 +29,61 @@ describe("applications", () => {
cy.contains("cypress_logging_events");
cy.contains("1 - 1 of 1");
});
it("can see application sidebar section always when filled", () => {
cy.login();
setApplicationFeatureFlag(false);
cy.goToDataset(
"urn:li:dataset:(urn:li:dataPlatform:hive,cypress_logging_events,PROD)",
"cypress_logging_events",
);
cy.contains("Cypress Accounts Application");
});
it("can see application sidebar section when empty if feature flag is on", () => {
cy.login();
setApplicationFeatureFlag(true);
cy.goToDataset(
"urn:li:dataset:(urn:li:dataPlatform:bigquery,cypress_project.jaffle_shop.customers,PROD)",
"customers",
);
cy.contains("No application yet");
});
it("cannot see application sidebar section when empty if feature flag is off", () => {
cy.login();
setApplicationFeatureFlag(false);
cy.goToDataset(
"urn:li:dataset:(urn:li:dataPlatform:bigquery,cypress_project.jaffle_shop.customers,PROD)",
"customers",
);
cy.ensureTextNotPresent("No application yet");
});
it("can add and remove application to dataset", () => {
cy.login();
setApplicationFeatureFlag(true);
cy.goToDataset(
"urn:li:dataset:(urn:li:dataPlatform:bigquery,cypress_project.jaffle_shop.customers,PROD)",
"customers",
);
cy.clickOptionWithTestId("add-applications-button");
cy.contains("Select an application").click({ force: true });
cy.focused().type("Cypress Accounts Application");
cy.get(".ant-select-item").contains("Cypress Accounts Application").click();
cy.clickOptionWithText("OK");
cy.waitTextVisible("Application set");
cy.contains("Cypress Accounts Application");
cy.removeApplicationFromDataset(
"urn:li:dataset:(urn:li:dataPlatform:bigquery,cypress_project.jaffle_shop.customers,PROD)",
"customers",
"urn:li:application:d63587c6-cacc-4590-851c-4f51ca429b51/Assets",
);
cy.ensureTextNotPresent("Cypress Accounts Application");
});
});

View File

@ -440,6 +440,15 @@ Cypress.Commands.add(
},
);
Cypress.Commands.add(
"removeApplicationFromDataset",
(urn, dataset_name, application_urn) => {
cy.goToDataset(urn, dataset_name);
cy.get(`.sidebar-application-section .anticon-close`).click();
cy.clickOptionWithText("Yes");
},
);
Cypress.Commands.add("openEntityTab", (tab) => {
const selector = `div[id$="${tab}"]:nth-child(1)`;
cy.get(selector).click();