mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-25 00:48:45 +00:00
feat(usergroup): implement corpgroup in graphql, refactor avatars and ownership in react (#2519)
This commit is contained in:
parent
2811d23e45
commit
d7d8870008
2
.gitignore
vendored
2
.gitignore
vendored
@ -34,3 +34,5 @@ MANIFEST
|
||||
|
||||
# Mac OS
|
||||
**/.DS_Store
|
||||
|
||||
.vscode
|
||||
@ -6,6 +6,7 @@ import com.linkedin.dataplatform.client.DataPlatforms;
|
||||
import com.linkedin.dataset.client.Datasets;
|
||||
import com.linkedin.identity.client.CorpUsers;
|
||||
import com.linkedin.lineage.client.Lineages;
|
||||
import com.linkedin.identity.client.CorpGroups;
|
||||
import com.linkedin.metadata.restli.DefaultRestliClientFactory;
|
||||
import com.linkedin.ml.client.MLModels;
|
||||
import com.linkedin.restli.client.Client;
|
||||
@ -38,6 +39,7 @@ public class GmsClientFactory {
|
||||
Configuration.getEnvironmentVariable(GMS_SSL_PROTOCOL_VAR));
|
||||
|
||||
private static CorpUsers _corpUsers;
|
||||
private static CorpGroups _corpGroups;
|
||||
private static Datasets _datasets;
|
||||
private static Dashboards _dashboards;
|
||||
private static Charts _charts;
|
||||
@ -63,6 +65,17 @@ public class GmsClientFactory {
|
||||
return _corpUsers;
|
||||
}
|
||||
|
||||
public static CorpGroups getCorpGroupsClient() {
|
||||
if (_corpGroups == null) {
|
||||
synchronized (GmsClientFactory.class) {
|
||||
if (_corpGroups == null) {
|
||||
_corpGroups = new CorpGroups(REST_CLIENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _corpGroups;
|
||||
}
|
||||
|
||||
public static Datasets getDatasetsClient() {
|
||||
if (_datasets == null) {
|
||||
synchronized (GmsClientFactory.class) {
|
||||
|
||||
@ -21,13 +21,17 @@ import com.linkedin.datahub.graphql.types.LoadableType;
|
||||
import com.linkedin.datahub.graphql.types.SearchableEntityType;
|
||||
import com.linkedin.datahub.graphql.types.chart.ChartType;
|
||||
import com.linkedin.datahub.graphql.types.corpuser.CorpUserType;
|
||||
import com.linkedin.datahub.graphql.types.corpgroup.CorpGroupType;
|
||||
import com.linkedin.datahub.graphql.types.dashboard.DashboardType;
|
||||
import com.linkedin.datahub.graphql.types.dataplatform.DataPlatformType;
|
||||
import com.linkedin.datahub.graphql.types.dataset.DatasetType;
|
||||
import com.linkedin.datahub.graphql.generated.CorpUser;
|
||||
import com.linkedin.datahub.graphql.generated.CorpUserInfo;
|
||||
import com.linkedin.datahub.graphql.generated.CorpGroupInfo;
|
||||
import com.linkedin.datahub.graphql.generated.Owner;
|
||||
import com.linkedin.datahub.graphql.resolvers.AuthenticatedResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.load.LoadableTypeResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.load.OwnerTypeResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.browse.BrowsePathsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.browse.BrowseResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.search.AutoCompleteResolver;
|
||||
@ -71,6 +75,7 @@ public class GmsGraphQLEngine {
|
||||
|
||||
public static final DatasetType DATASET_TYPE = new DatasetType(GmsClientFactory.getDatasetsClient());
|
||||
public static final CorpUserType CORP_USER_TYPE = new CorpUserType(GmsClientFactory.getCorpUsersClient());
|
||||
public static final CorpGroupType CORP_GROUP_TYPE = new CorpGroupType(GmsClientFactory.getCorpGroupsClient());
|
||||
public static final ChartType CHART_TYPE = new ChartType(GmsClientFactory.getChartsClient());
|
||||
public static final DashboardType DASHBOARD_TYPE = new DashboardType(GmsClientFactory.getDashboardsClient());
|
||||
public static final DataPlatformType DATA_PLATFORM_TYPE = new DataPlatformType(GmsClientFactory.getDataPlatformsClient());
|
||||
@ -92,6 +97,7 @@ public class GmsGraphQLEngine {
|
||||
public static final List<EntityType<?>> ENTITY_TYPES = ImmutableList.of(
|
||||
DATASET_TYPE,
|
||||
CORP_USER_TYPE,
|
||||
CORP_GROUP_TYPE,
|
||||
DATA_PLATFORM_TYPE,
|
||||
CHART_TYPE,
|
||||
DASHBOARD_TYPE,
|
||||
@ -115,7 +121,13 @@ public class GmsGraphQLEngine {
|
||||
*/
|
||||
public static final List<LoadableType<?>> LOADABLE_TYPES = Stream.concat(ENTITY_TYPES.stream(), RELATIONSHIP_TYPES.stream()).collect(Collectors.toList());
|
||||
|
||||
|
||||
/**
|
||||
* Configures the graph objects for owner
|
||||
*/
|
||||
public static final List<LoadableType<?>> OWNER_TYPES = ImmutableList.of(
|
||||
CORP_USER_TYPE,
|
||||
CORP_GROUP_TYPE
|
||||
);
|
||||
|
||||
/**
|
||||
* Configures the graph objects that can be searched.
|
||||
@ -163,6 +175,7 @@ public class GmsGraphQLEngine {
|
||||
configureMutationResolvers(builder);
|
||||
configureDatasetResolvers(builder);
|
||||
configureCorpUserResolvers(builder);
|
||||
configureCorpGroupResolvers(builder);
|
||||
configureDashboardResolvers(builder);
|
||||
configureChartResolvers(builder);
|
||||
configureTypeResolvers(builder);
|
||||
@ -207,6 +220,10 @@ public class GmsGraphQLEngine {
|
||||
new LoadableTypeResolver<>(
|
||||
CORP_USER_TYPE,
|
||||
(env) -> env.getArgument(URN_FIELD_NAME))))
|
||||
.dataFetcher("corpGroup", new AuthenticatedResolver<>(
|
||||
new LoadableTypeResolver<>(
|
||||
CORP_GROUP_TYPE,
|
||||
(env) -> env.getArgument(URN_FIELD_NAME))))
|
||||
.dataFetcher("dashboard", new AuthenticatedResolver<>(
|
||||
new LoadableTypeResolver<>(
|
||||
DASHBOARD_TYPE,
|
||||
@ -269,9 +286,9 @@ public class GmsGraphQLEngine {
|
||||
)
|
||||
.type("Owner", typeWiring -> typeWiring
|
||||
.dataFetcher("owner", new AuthenticatedResolver<>(
|
||||
new LoadableTypeResolver<>(
|
||||
CORP_USER_TYPE,
|
||||
(env) -> ((Owner) env.getSource()).getOwner().getUrn()))
|
||||
new OwnerTypeResolver<>(
|
||||
OWNER_TYPES,
|
||||
(env) -> ((Owner) env.getSource()).getOwner()))
|
||||
)
|
||||
)
|
||||
.type("RelatedDataset", typeWiring -> typeWiring
|
||||
@ -285,8 +302,7 @@ public class GmsGraphQLEngine {
|
||||
.dataFetcher("entity", new AuthenticatedResolver<>(
|
||||
new EntityTypeResolver(
|
||||
ENTITY_TYPES.stream().collect(Collectors.toList()),
|
||||
(env) -> ((EntityRelationship) env.getSource()).getEntity())
|
||||
)
|
||||
(env) -> ((EntityRelationship) env.getSource()).getEntity()))
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -304,6 +320,28 @@ public class GmsGraphQLEngine {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures resolvers responsible for resolving the {@link com.linkedin.datahub.graphql.generated.CorpGroup} type.
|
||||
*/
|
||||
private static void configureCorpGroupResolvers(final RuntimeWiring.Builder builder) {
|
||||
builder.type("CorpGroupInfo", typeWiring -> typeWiring
|
||||
.dataFetcher("admins", new AuthenticatedResolver<>(
|
||||
new LoadableTypeBatchResolver<>(
|
||||
CORP_USER_TYPE,
|
||||
(env) -> ((CorpGroupInfo) env.getSource()).getAdmins().stream()
|
||||
.map(CorpUser::getUrn)
|
||||
.collect(Collectors.toList())))
|
||||
)
|
||||
.dataFetcher("members", new AuthenticatedResolver<>(
|
||||
new LoadableTypeBatchResolver<>(
|
||||
CORP_USER_TYPE,
|
||||
(env) -> ((CorpGroupInfo) env.getSource()).getMembers().stream()
|
||||
.map(CorpUser::getUrn)
|
||||
.collect(Collectors.toList())))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static void configureTagAssociationResolver(final RuntimeWiring.Builder builder) {
|
||||
builder.type("TagAssociation", typeWiring -> typeWiring
|
||||
.dataFetcher("tag", new AuthenticatedResolver<>(
|
||||
@ -379,12 +417,18 @@ public class GmsGraphQLEngine {
|
||||
.map(graphType -> (EntityType<?>) graphType)
|
||||
.collect(Collectors.toList())
|
||||
)))
|
||||
.type("EntityWithRelationships", typeWiring -> typeWiring
|
||||
.typeResolver(new EntityInterfaceTypeResolver(LOADABLE_TYPES.stream()
|
||||
.filter(graphType -> graphType instanceof EntityType)
|
||||
.map(graphType -> (EntityType<?>) graphType)
|
||||
.collect(Collectors.toList())
|
||||
)))
|
||||
.type("EntityWithRelationships", typeWiring -> typeWiring
|
||||
.typeResolver(new EntityInterfaceTypeResolver(LOADABLE_TYPES.stream()
|
||||
.filter(graphType -> graphType instanceof EntityType)
|
||||
.map(graphType -> (EntityType<?>) graphType)
|
||||
.collect(Collectors.toList())
|
||||
)))
|
||||
.type("OwnerType", typeWiring -> typeWiring
|
||||
.typeResolver(new EntityInterfaceTypeResolver(OWNER_TYPES.stream()
|
||||
.filter(graphType -> graphType instanceof EntityType)
|
||||
.map(graphType -> (EntityType<?>) graphType)
|
||||
.collect(Collectors.toList())
|
||||
)))
|
||||
.type("PlatformSchema", typeWiring -> typeWiring
|
||||
.typeResolver(new PlatformSchemaUnionTypeResolver())
|
||||
)
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.load;
|
||||
|
||||
import com.linkedin.datahub.graphql.generated.Entity;
|
||||
import com.linkedin.datahub.graphql.generated.OwnerType;
|
||||
import com.linkedin.datahub.graphql.types.LoadableType;
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Function;
|
||||
import org.dataloader.DataLoader;
|
||||
import java.util.stream.Collectors;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
/**
|
||||
* Generic GraphQL resolver responsible for
|
||||
*
|
||||
* 1. Retrieving a single input urn.
|
||||
* 2. Resolving a single {@link LoadableType}.
|
||||
*
|
||||
* Note that this resolver expects that {@link DataLoader}s were registered
|
||||
* for the provided {@link LoadableType} under the name provided by {@link LoadableType#name()}
|
||||
*
|
||||
* @param <T> the generated GraphQL POJO corresponding to the resolved type.
|
||||
*/
|
||||
public class OwnerTypeResolver<T> implements DataFetcher<CompletableFuture<T>> {
|
||||
|
||||
private final List<LoadableType<?>> _loadableTypes;
|
||||
private final Function<DataFetchingEnvironment, OwnerType> _urnProvider;
|
||||
|
||||
public OwnerTypeResolver(final List<LoadableType<?>> loadableTypes, final Function<DataFetchingEnvironment, OwnerType> urnProvider) {
|
||||
_loadableTypes = loadableTypes;
|
||||
_urnProvider = urnProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<T> get(DataFetchingEnvironment environment) {
|
||||
final OwnerType ownerType = _urnProvider.apply(environment);
|
||||
final LoadableType<?> filteredEntity = Iterables.getOnlyElement(_loadableTypes.stream()
|
||||
.filter(entity -> ownerType.getClass().isAssignableFrom(entity.objectClass()))
|
||||
.collect(Collectors.toList()));
|
||||
final DataLoader<String, T> loader = environment.getDataLoaderRegistry().getDataLoader(filteredEntity.name());
|
||||
return loader.load(((Entity) ownerType).getUrn());
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package com.linkedin.datahub.graphql.types.common.mappers;
|
||||
|
||||
import com.linkedin.datahub.graphql.generated.CorpUser;
|
||||
import com.linkedin.datahub.graphql.generated.CorpGroup;
|
||||
import com.linkedin.datahub.graphql.generated.Owner;
|
||||
import com.linkedin.datahub.graphql.generated.OwnershipType;
|
||||
import com.linkedin.datahub.graphql.types.mappers.ModelMapper;
|
||||
@ -24,9 +25,15 @@ public class OwnerMapper implements ModelMapper<com.linkedin.common.Owner, Owner
|
||||
public Owner apply(@Nonnull final com.linkedin.common.Owner owner) {
|
||||
final Owner result = new Owner();
|
||||
result.setType(Enum.valueOf(OwnershipType.class, owner.getType().toString()));
|
||||
CorpUser partialOwner = new CorpUser();
|
||||
partialOwner.setUrn(owner.getOwner().toString());
|
||||
result.setOwner(partialOwner);
|
||||
if (owner.getOwner().getEntityType().equals("corpuser")) {
|
||||
CorpUser partialOwner = new CorpUser();
|
||||
partialOwner.setUrn(owner.getOwner().toString());
|
||||
result.setOwner(partialOwner);
|
||||
} else {
|
||||
CorpGroup partialOwner = new CorpGroup();
|
||||
partialOwner.setUrn(owner.getOwner().toString());
|
||||
result.setOwner(partialOwner);
|
||||
}
|
||||
if (owner.hasSource()) {
|
||||
result.setSource(OwnershipSourceMapper.map(owner.getSource()));
|
||||
}
|
||||
|
||||
@ -8,7 +8,11 @@ import com.linkedin.common.OwnershipSourceType;
|
||||
import com.linkedin.common.OwnershipType;
|
||||
import com.linkedin.datahub.graphql.generated.OwnerUpdate;
|
||||
import com.linkedin.datahub.graphql.types.corpuser.CorpUserUtils;
|
||||
import com.linkedin.datahub.graphql.types.corpgroup.CorpGroupUtils;
|
||||
import com.linkedin.datahub.graphql.types.mappers.ModelMapper;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
public class OwnerUpdateMapper implements ModelMapper<OwnerUpdate, Owner> {
|
||||
|
||||
@ -21,7 +25,15 @@ public class OwnerUpdateMapper implements ModelMapper<OwnerUpdate, Owner> {
|
||||
@Override
|
||||
public Owner apply(@Nonnull final OwnerUpdate input) {
|
||||
final Owner owner = new Owner();
|
||||
owner.setOwner(CorpUserUtils.getCorpUserUrn(input.getOwner()));
|
||||
try {
|
||||
if (Urn.createFromString(input.getOwner()).getEntityType().equals("corpuser")) {
|
||||
owner.setOwner(CorpUserUtils.getCorpUserUrn(input.getOwner()));
|
||||
} else if (Urn.createFromString(input.getOwner()).getEntityType().equals("corpGroup")) {
|
||||
owner.setOwner(CorpGroupUtils.getCorpGroupUrn(input.getOwner()));
|
||||
}
|
||||
} catch (URISyntaxException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
owner.setType(OwnershipType.valueOf(input.getType().toString()));
|
||||
owner.setSource(new OwnershipSource().setType(OwnershipSourceType.SERVICE));
|
||||
return owner;
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
package com.linkedin.datahub.graphql.types.corpgroup;
|
||||
|
||||
import com.linkedin.common.urn.CorpGroupUrn;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.generated.EntityType;
|
||||
import com.linkedin.datahub.graphql.types.SearchableEntityType;
|
||||
import com.linkedin.datahub.graphql.generated.AutoCompleteResults;
|
||||
import com.linkedin.datahub.graphql.generated.CorpGroup;
|
||||
import com.linkedin.datahub.graphql.generated.FacetFilterInput;
|
||||
import com.linkedin.datahub.graphql.generated.SearchResults;
|
||||
import com.linkedin.datahub.graphql.types.mappers.AutoCompleteResultsMapper;
|
||||
import com.linkedin.datahub.graphql.types.corpgroup.mappers.CorpGroupMapper;
|
||||
import com.linkedin.datahub.graphql.types.mappers.SearchResultsMapper;
|
||||
import com.linkedin.identity.client.CorpGroups;
|
||||
import com.linkedin.metadata.query.AutoCompleteResult;
|
||||
import com.linkedin.restli.common.CollectionResponse;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CorpGroupType implements SearchableEntityType<CorpGroup> {
|
||||
|
||||
private static final String DEFAULT_AUTO_COMPLETE_FIELD = "name";
|
||||
|
||||
private final CorpGroups _corpGroupsClient;
|
||||
|
||||
public CorpGroupType(final CorpGroups corpGroupsClient) {
|
||||
_corpGroupsClient = corpGroupsClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<CorpGroup> objectClass() {
|
||||
return CorpGroup.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityType type() {
|
||||
return EntityType.CORP_GROUP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CorpGroup> batchLoad(final List<String> urns, final QueryContext context) {
|
||||
try {
|
||||
final List<CorpGroupUrn> corpGroupUrns = urns
|
||||
.stream()
|
||||
.map(this::getCorpGroupUrn)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
final Map<CorpGroupUrn, com.linkedin.identity.CorpGroup> corpGroupMap = _corpGroupsClient
|
||||
.batchGet(new HashSet<>(corpGroupUrns));
|
||||
|
||||
final List<com.linkedin.identity.CorpGroup> results = new ArrayList<>();
|
||||
for (CorpGroupUrn urn : corpGroupUrns) {
|
||||
results.add(corpGroupMap.getOrDefault(urn, null));
|
||||
}
|
||||
return results.stream()
|
||||
.map(gmsCorpGroup -> gmsCorpGroup == null ? null : CorpGroupMapper.map(gmsCorpGroup))
|
||||
.collect(Collectors.toList());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to batch load CorpGroup", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResults search(@Nonnull String query,
|
||||
@Nullable List<FacetFilterInput> filters,
|
||||
int start,
|
||||
int count,
|
||||
@Nonnull final QueryContext context) throws Exception {
|
||||
final CollectionResponse<com.linkedin.identity.CorpGroup> searchResult = _corpGroupsClient.search(query, Collections.emptyMap(), start, count);
|
||||
return SearchResultsMapper.map(searchResult, CorpGroupMapper::map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AutoCompleteResults autoComplete(@Nonnull String query,
|
||||
@Nullable String field,
|
||||
@Nullable List<FacetFilterInput> filters,
|
||||
int limit,
|
||||
@Nonnull final QueryContext context) throws Exception {
|
||||
field = field != null ? field : DEFAULT_AUTO_COMPLETE_FIELD;
|
||||
final AutoCompleteResult result = _corpGroupsClient.autocomplete(query, field, Collections.emptyMap(), limit);
|
||||
return AutoCompleteResultsMapper.map(result);
|
||||
}
|
||||
|
||||
private CorpGroupUrn getCorpGroupUrn(final String urnStr) {
|
||||
try {
|
||||
return CorpGroupUrn.createFromString(urnStr);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException(String.format("Failed to retrieve CorpGroup with urn %s, invalid urn", urnStr));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.linkedin.datahub.graphql.types.corpgroup;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
import com.linkedin.common.urn.CorpGroupUrn;
|
||||
|
||||
public class CorpGroupUtils {
|
||||
|
||||
private CorpGroupUtils() { }
|
||||
|
||||
public static CorpGroupUrn getCorpGroupUrn(final String urnStr) {
|
||||
if (urnStr == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return CorpGroupUrn.createFromString(urnStr);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException(String.format("Failed to create CorpGroupUrn from string %s", urnStr), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package com.linkedin.datahub.graphql.types.corpgroup.mappers;
|
||||
|
||||
import com.linkedin.datahub.graphql.generated.CorpUser;
|
||||
import com.linkedin.datahub.graphql.generated.CorpGroupInfo;
|
||||
import com.linkedin.datahub.graphql.types.mappers.ModelMapper;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Maps Pegasus {@link RecordTemplate} objects to objects conforming to the GQL schema.
|
||||
*
|
||||
* To be replaced by auto-generated mappers implementations
|
||||
*/
|
||||
public class CorpGroupInfoMapper implements ModelMapper<com.linkedin.identity.CorpGroupInfo, CorpGroupInfo> {
|
||||
|
||||
public static final CorpGroupInfoMapper INSTANCE = new CorpGroupInfoMapper();
|
||||
|
||||
public static CorpGroupInfo map(@Nonnull final com.linkedin.identity.CorpGroupInfo corpGroupInfo) {
|
||||
return INSTANCE.apply(corpGroupInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CorpGroupInfo apply(@Nonnull final com.linkedin.identity.CorpGroupInfo info) {
|
||||
final CorpGroupInfo result = new CorpGroupInfo();
|
||||
result.setEmail(info.getEmail());
|
||||
if (info.hasAdmins()) {
|
||||
result.setAdmins(info.getAdmins().stream().map(urn -> {
|
||||
final CorpUser corpUser = new CorpUser();
|
||||
corpUser.setUrn(urn.toString());
|
||||
return corpUser;
|
||||
}).collect(Collectors.toList()));
|
||||
}
|
||||
if (info.hasMembers()) {
|
||||
result.setMembers(info.getMembers().stream().map(urn -> {
|
||||
final CorpUser corpUser = new CorpUser();
|
||||
corpUser.setUrn(urn.toString());
|
||||
return corpUser;
|
||||
}).collect(Collectors.toList()));
|
||||
}
|
||||
if (info.hasGroups()) {
|
||||
result.setGroups(info.getGroups().stream().map(urn -> (urn.toString())).collect(Collectors.toList()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.linkedin.datahub.graphql.types.corpgroup.mappers;
|
||||
|
||||
import com.linkedin.common.urn.CorpGroupUrn;
|
||||
import com.linkedin.datahub.graphql.generated.CorpGroup;
|
||||
import com.linkedin.datahub.graphql.generated.EntityType;
|
||||
import com.linkedin.datahub.graphql.types.mappers.ModelMapper;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* Maps Pegasus {@link RecordTemplate} objects to objects conforming to the GQL schema.
|
||||
*
|
||||
* To be replaced by auto-generated mappers implementations
|
||||
*/
|
||||
public class CorpGroupMapper implements ModelMapper<com.linkedin.identity.CorpGroup, CorpGroup> {
|
||||
|
||||
public static final CorpGroupMapper INSTANCE = new CorpGroupMapper();
|
||||
|
||||
public static CorpGroup map(@Nonnull final com.linkedin.identity.CorpGroup corpGroup) {
|
||||
return INSTANCE.apply(corpGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CorpGroup apply(@Nonnull final com.linkedin.identity.CorpGroup corpGroup) {
|
||||
final CorpGroup result = new CorpGroup();
|
||||
result.setUrn(new CorpGroupUrn(corpGroup.getName()).toString());
|
||||
result.setType(EntityType.CORP_GROUP);
|
||||
result.setName(corpGroup.getName());
|
||||
if (corpGroup.hasInfo()) {
|
||||
result.setInfo(CorpGroupInfoMapper.map(corpGroup.getInfo()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@ -56,6 +56,10 @@ enum EntityType {
|
||||
"""
|
||||
CORP_USER
|
||||
"""
|
||||
The CorpGroup Entity
|
||||
"""
|
||||
CORP_GROUP
|
||||
"""
|
||||
The DataPlatform Entity
|
||||
"""
|
||||
DATA_PLATFORM
|
||||
@ -100,6 +104,11 @@ type Query {
|
||||
"""
|
||||
corpUser(urn: String!): CorpUser
|
||||
|
||||
"""
|
||||
Fetch a CorpGroup by primary key
|
||||
"""
|
||||
corpGroup(urn: String!): CorpGroup
|
||||
|
||||
"""
|
||||
Fetch a Dashboard by primary key
|
||||
"""
|
||||
@ -732,18 +741,6 @@ type StringMapEntry {
|
||||
value: String
|
||||
}
|
||||
|
||||
type Ownership {
|
||||
"""
|
||||
List of owners of the entity
|
||||
"""
|
||||
owners: [Owner!]
|
||||
|
||||
"""
|
||||
Audit stamp containing who last modified the record and when
|
||||
"""
|
||||
lastModified: AuditStamp!
|
||||
}
|
||||
|
||||
enum OwnershipSourceType {
|
||||
"""
|
||||
Auditing system or audit logs
|
||||
@ -823,23 +820,6 @@ enum OwnershipType {
|
||||
STAKEHOLDER
|
||||
}
|
||||
|
||||
type Owner {
|
||||
"""
|
||||
Owner object - This should be extended to support CorpGroups as well
|
||||
"""
|
||||
owner: CorpUser!
|
||||
|
||||
"""
|
||||
The type of the ownership
|
||||
"""
|
||||
type: OwnershipType!
|
||||
|
||||
"""
|
||||
Source information for the ownership
|
||||
"""
|
||||
source: OwnershipSource
|
||||
}
|
||||
|
||||
type CorpUser implements Entity {
|
||||
"""
|
||||
The unique user URN
|
||||
@ -951,7 +931,82 @@ type CorpUserEditableInfo {
|
||||
pictureLink: String
|
||||
}
|
||||
|
||||
type Tag implements Entity{
|
||||
type CorpGroup implements Entity {
|
||||
"""
|
||||
The unique user URN
|
||||
"""
|
||||
urn: String!
|
||||
|
||||
"""
|
||||
GMS Entity Type
|
||||
"""
|
||||
type: EntityType!
|
||||
|
||||
"""
|
||||
group name e.g. wherehows-dev, ask_metadata
|
||||
"""
|
||||
name: String
|
||||
|
||||
"""
|
||||
Information of the corp group
|
||||
"""
|
||||
info: CorpGroupInfo
|
||||
}
|
||||
|
||||
type CorpGroupInfo {
|
||||
"""
|
||||
email of this group
|
||||
"""
|
||||
email: String!
|
||||
|
||||
"""
|
||||
owners of this group
|
||||
"""
|
||||
admins: [CorpUser!]
|
||||
|
||||
"""
|
||||
List of ldap urn in this group.
|
||||
"""
|
||||
members: [CorpUser!]
|
||||
|
||||
"""
|
||||
List of groups in this group.
|
||||
"""
|
||||
groups: [String!]
|
||||
}
|
||||
|
||||
union OwnerType = CorpUser | CorpGroup
|
||||
|
||||
type Owner {
|
||||
"""
|
||||
Owner object
|
||||
"""
|
||||
owner: OwnerType!
|
||||
|
||||
"""
|
||||
The type of the ownership
|
||||
"""
|
||||
type: OwnershipType!
|
||||
|
||||
"""
|
||||
Source information for the ownership
|
||||
"""
|
||||
source: OwnershipSource
|
||||
}
|
||||
|
||||
type Ownership {
|
||||
"""
|
||||
List of owners of the entity
|
||||
"""
|
||||
owners: [Owner!]
|
||||
|
||||
"""
|
||||
Audit stamp containing who last modified the record and when
|
||||
"""
|
||||
lastModified: AuditStamp!
|
||||
}
|
||||
|
||||
type Tag implements Entity {
|
||||
urn: String!
|
||||
"""
|
||||
GMS Entity Type
|
||||
|
||||
@ -238,11 +238,58 @@ type CorpUser {
|
||||
editableInfo: CorpUserEditableInfo
|
||||
}
|
||||
|
||||
type CorpGroup implements Entity {
|
||||
"""
|
||||
The unique user URN
|
||||
"""
|
||||
urn: String!
|
||||
|
||||
"""
|
||||
GMS Entity Type
|
||||
"""
|
||||
type: EntityType!
|
||||
|
||||
"""
|
||||
group name e.g. wherehows-dev, ask_metadata
|
||||
"""
|
||||
name: String
|
||||
|
||||
"""
|
||||
Information of the corp group
|
||||
"""
|
||||
info: CorpGroupInfo
|
||||
}
|
||||
|
||||
|
||||
type CorpGroupInfo {
|
||||
"""
|
||||
email of this group
|
||||
"""
|
||||
email: String!
|
||||
|
||||
"""
|
||||
owners of this group
|
||||
"""
|
||||
admins: [String!]!
|
||||
|
||||
"""
|
||||
List of ldap urn in this group.
|
||||
"""
|
||||
members: [String!]!
|
||||
|
||||
"""
|
||||
List of groups in this group.
|
||||
"""
|
||||
groups: [String!]!
|
||||
}
|
||||
|
||||
enum EntityType {
|
||||
DATASET
|
||||
USER
|
||||
DATA_FLOW
|
||||
DATA_JOB
|
||||
CORP_USER
|
||||
CORP_GROUP
|
||||
}
|
||||
|
||||
# Search Input
|
||||
|
||||
@ -13,6 +13,7 @@ import EntityRegistry from './app/entity/EntityRegistry';
|
||||
import { DashboardEntity } from './app/entity/dashboard/DashboardEntity';
|
||||
import { ChartEntity } from './app/entity/chart/ChartEntity';
|
||||
import { UserEntity } from './app/entity/user/User';
|
||||
import { UserGroupEntity } from './app/entity/userGroup/UserGroup';
|
||||
import { DatasetEntity } from './app/entity/dataset/DatasetEntity';
|
||||
import { DataFlowEntity } from './app/entity/dataFlow/DataFlowEntity';
|
||||
import { DataJobEntity } from './app/entity/dataJob/DataJobEntity';
|
||||
@ -88,6 +89,7 @@ const App: React.VFC = () => {
|
||||
register.register(new DashboardEntity());
|
||||
register.register(new ChartEntity());
|
||||
register.register(new UserEntity());
|
||||
register.register(new UserGroupEntity());
|
||||
register.register(new TagEntity());
|
||||
register.register(new DataFlowEntity());
|
||||
register.register(new DataJobEntity());
|
||||
|
||||
@ -35,15 +35,7 @@ export const ChartPreview = ({
|
||||
platform={capitalizedPlatform}
|
||||
qualifier={access}
|
||||
tags={tags}
|
||||
owners={
|
||||
owners?.map((owner) => {
|
||||
return {
|
||||
urn: owner.owner.urn,
|
||||
name: owner.owner.info?.fullName || '',
|
||||
photoUrl: owner.owner.editableInfo?.pictureLink || '',
|
||||
};
|
||||
}) || []
|
||||
}
|
||||
owners={owners}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Avatar, Button, Divider, Row, Space, Typography } from 'antd';
|
||||
import { Button, Divider, Row, Space, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import { AuditStamp, ChartType, EntityType, Ownership } from '../../../../types.generated';
|
||||
import { AuditStamp, ChartType, Ownership } from '../../../../types.generated';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import CustomAvatar from '../../../shared/avatar/CustomAvatar';
|
||||
import { AvatarsGroup } from '../../../shared/avatar';
|
||||
import { capitalizeFirstLetter } from '../../../shared/capitalizeFirstLetter';
|
||||
import analytics, { EventType, EntityActionType } from '../../../analytics';
|
||||
|
||||
@ -57,16 +57,7 @@ export default function ChartHeader({
|
||||
</Space>
|
||||
</Row>
|
||||
<Typography.Paragraph>{description}</Typography.Paragraph>
|
||||
<Avatar.Group maxCount={6} size="large">
|
||||
{ownership?.owners?.map((owner: any) => (
|
||||
<CustomAvatar
|
||||
key={owner.owner.urn}
|
||||
name={owner.owner.info?.fullName}
|
||||
url={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${owner.owner.urn}`}
|
||||
photoUrl={owner.owner.editableInfo?.pictureLink}
|
||||
/>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
<AvatarsGroup owners={ownership?.owners} entityRegistry={entityRegistry} size="large" />
|
||||
{lastModified && (
|
||||
<Typography.Text type="secondary">
|
||||
Last modified at {new Date(lastModified.time).toLocaleDateString('en-US')}
|
||||
|
||||
@ -34,15 +34,7 @@ export const DashboardPreview = ({
|
||||
logoUrl={getLogoFromPlatform(platform) || ''}
|
||||
platform={capitalizedPlatform}
|
||||
qualifier={access}
|
||||
owners={
|
||||
owners?.map((owner) => {
|
||||
return {
|
||||
urn: owner.owner.urn,
|
||||
name: owner.owner.info?.fullName || '',
|
||||
photoUrl: owner.owner.editableInfo?.pictureLink || '',
|
||||
};
|
||||
}) || []
|
||||
}
|
||||
owners={owners}
|
||||
tags={tags}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { Avatar, Button, Divider, Row, Space, Typography } from 'antd';
|
||||
import { Button, Divider, Row, Space, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import { AuditStamp, EntityType, Ownership } from '../../../../types.generated';
|
||||
import { AuditStamp, Ownership } from '../../../../types.generated';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import CustomAvatar from '../../../shared/avatar/CustomAvatar';
|
||||
import { capitalizeFirstLetter } from '../../../shared/capitalizeFirstLetter';
|
||||
import { AvatarsGroup } from '../../../shared/avatar';
|
||||
import analytics, { EventType, EntityActionType } from '../../../analytics';
|
||||
|
||||
const styles = {
|
||||
@ -45,16 +45,7 @@ export default function DashboardHeader({ urn, platform, description, ownership,
|
||||
</Space>
|
||||
</Row>
|
||||
<Typography.Paragraph>{description}</Typography.Paragraph>
|
||||
<Avatar.Group maxCount={6} size="large">
|
||||
{ownership?.owners?.map((owner: any) => (
|
||||
<CustomAvatar
|
||||
key={owner.owner.urn}
|
||||
name={owner.owner.info?.fullName}
|
||||
url={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${owner.owner.urn}`}
|
||||
photoUrl={owner.owner.editableInfo?.pictureLink}
|
||||
/>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
<AvatarsGroup owners={ownership?.owners} entityRegistry={entityRegistry} size="large" />
|
||||
{lastModified && (
|
||||
<Typography.Text type="secondary">
|
||||
Last modified at {new Date(lastModified.time).toLocaleDateString('en-US')}
|
||||
|
||||
@ -33,15 +33,7 @@ export const Preview = ({
|
||||
type="Data Pipeline"
|
||||
platform={capitalizedPlatform}
|
||||
logoUrl={platformLogo || ''}
|
||||
owners={
|
||||
owners?.map((owner) => {
|
||||
return {
|
||||
urn: owner.owner.urn,
|
||||
name: owner.owner.info?.fullName || '',
|
||||
photoUrl: owner.owner.editableInfo?.pictureLink || '',
|
||||
};
|
||||
}) || []
|
||||
}
|
||||
owners={owners}
|
||||
tags={globalTags || undefined}
|
||||
snippet={snippet}
|
||||
/>
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { Avatar, Button, Divider, Row, Space, Tooltip, Typography } from 'antd';
|
||||
import { Button, Divider, Row, Space, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { DataFlow, EntityType } from '../../../../types.generated';
|
||||
import { DataFlow } from '../../../../types.generated';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import defaultAvatar from '../../../../images/default_avatar.png';
|
||||
import { capitalizeFirstLetter } from '../../../shared/capitalizeFirstLetter';
|
||||
import { AvatarsGroup } from '../../../shared/avatar';
|
||||
import analytics, { EventType, EntityActionType } from '../../../analytics';
|
||||
|
||||
export type Props = {
|
||||
@ -36,24 +35,7 @@ export default function DataFlowHeader({ dataFlow: { urn, ownership, info, orche
|
||||
</Space>
|
||||
</Row>
|
||||
<Typography.Paragraph>{info?.description}</Typography.Paragraph>
|
||||
<Avatar.Group maxCount={6} size="large">
|
||||
{ownership?.owners?.map((owner) => (
|
||||
<Tooltip title={owner.owner.info?.fullName} key={owner.owner.urn}>
|
||||
<Link to={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${owner.owner.urn}`}>
|
||||
<Avatar
|
||||
style={{
|
||||
color: '#f56a00',
|
||||
backgroundColor: '#fde3cf',
|
||||
}}
|
||||
src={
|
||||
(owner.owner.editableInfo && owner.owner.editableInfo.pictureLink) ||
|
||||
defaultAvatar
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
<AvatarsGroup owners={ownership?.owners} entityRegistry={entityRegistry} size="large" />
|
||||
</Space>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -33,15 +33,7 @@ export const Preview = ({
|
||||
type="Data Task"
|
||||
platform={capitalizedPlatform}
|
||||
logoUrl={platformLogo || ''}
|
||||
owners={
|
||||
owners?.map((owner) => {
|
||||
return {
|
||||
urn: owner.owner.urn,
|
||||
name: owner.owner.info?.fullName || '',
|
||||
photoUrl: owner.owner.editableInfo?.pictureLink || '',
|
||||
};
|
||||
}) || []
|
||||
}
|
||||
owners={owners}
|
||||
tags={globalTags || undefined}
|
||||
snippet={snippet}
|
||||
/>
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { Avatar, Button, Divider, Row, Space, Tooltip, Typography } from 'antd';
|
||||
import { Button, Divider, Row, Space, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { DataJob, EntityType } from '../../../../types.generated';
|
||||
import { DataJob } from '../../../../types.generated';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import defaultAvatar from '../../../../images/default_avatar.png';
|
||||
import { capitalizeFirstLetter } from '../../../shared/capitalizeFirstLetter';
|
||||
import { AvatarsGroup } from '../../../shared/avatar';
|
||||
import analytics, { EventType, EntityActionType } from '../../../analytics';
|
||||
|
||||
export type Props = {
|
||||
@ -36,24 +35,7 @@ export default function DataJobHeader({ dataJob: { urn, ownership, info, dataFlo
|
||||
</Space>
|
||||
</Row>
|
||||
<Typography.Paragraph>{info?.description}</Typography.Paragraph>
|
||||
<Avatar.Group maxCount={6} size="large">
|
||||
{ownership?.owners?.map((owner) => (
|
||||
<Tooltip title={owner.owner.info?.fullName} key={owner.owner.urn}>
|
||||
<Link to={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${owner.owner.urn}`}>
|
||||
<Avatar
|
||||
style={{
|
||||
color: '#f56a00',
|
||||
backgroundColor: '#fde3cf',
|
||||
}}
|
||||
src={
|
||||
(owner.owner.editableInfo && owner.owner.editableInfo.pictureLink) ||
|
||||
defaultAvatar
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
<AvatarsGroup owners={ownership?.owners} entityRegistry={entityRegistry} size="large" />
|
||||
</Space>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -37,15 +37,7 @@ export const Preview = ({
|
||||
platform={capitalPlatformName}
|
||||
qualifier={origin}
|
||||
tags={globalTags || undefined}
|
||||
owners={
|
||||
owners?.map((owner) => {
|
||||
return {
|
||||
urn: owner.owner.urn,
|
||||
name: owner.owner.info?.fullName || '',
|
||||
photoUrl: owner.owner.editableInfo?.pictureLink || '',
|
||||
};
|
||||
}) || []
|
||||
}
|
||||
owners={owners}
|
||||
snippet={snippet}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Avatar, Badge, Divider, Popover, Space, Typography } from 'antd';
|
||||
import { Badge, Divider, Popover, Space, Typography } from 'antd';
|
||||
import { ParagraphProps } from 'antd/lib/typography/Paragraph';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Dataset, EntityType } from '../../../../types.generated';
|
||||
import { Dataset } from '../../../../types.generated';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import CustomAvatar from '../../../shared/avatar/CustomAvatar';
|
||||
import { AvatarsGroup } from '../../../shared/avatar';
|
||||
import CompactContext from '../../../shared/CompactContext';
|
||||
import { capitalizeFirstLetter } from '../../../shared/capitalizeFirstLetter';
|
||||
|
||||
@ -38,16 +38,7 @@ export default function DatasetHeader({ dataset: { description, ownership, depre
|
||||
<Typography.Text strong>{platformName}</Typography.Text>
|
||||
</Space>
|
||||
<DescriptionText isCompact={isCompact}>{description}</DescriptionText>
|
||||
<Avatar.Group maxCount={6} size="large">
|
||||
{ownership?.owners?.map((owner) => (
|
||||
<CustomAvatar
|
||||
key={owner.owner.urn}
|
||||
name={owner.owner.info?.fullName || undefined}
|
||||
url={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${owner.owner.urn}`}
|
||||
photoUrl={owner.owner.editableInfo?.pictureLink || undefined}
|
||||
/>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
<AvatarsGroup owners={ownership?.owners} entityRegistry={entityRegistry} size="large" />
|
||||
<div>
|
||||
{deprecation?.deprecated && (
|
||||
<Popover
|
||||
|
||||
@ -15,7 +15,10 @@ export const sampleDataset: Dataset = {
|
||||
tags: [],
|
||||
ownership: {
|
||||
owners: [
|
||||
{ owner: { urn: 'user:urn', type: EntityType.CorpUser, username: 'UserA' }, type: OwnershipType.Dataowner },
|
||||
{
|
||||
owner: { urn: 'user:urn', type: EntityType.CorpUser, username: 'UserA' },
|
||||
type: OwnershipType.Dataowner,
|
||||
},
|
||||
],
|
||||
lastModified: { time: 1 },
|
||||
},
|
||||
@ -53,7 +56,10 @@ export const sampleDeprecatedDataset: Dataset = {
|
||||
tags: [],
|
||||
ownership: {
|
||||
owners: [
|
||||
{ owner: { urn: 'user:urn', type: EntityType.CorpUser, username: 'UserA' }, type: OwnershipType.Dataowner },
|
||||
{
|
||||
owner: { urn: 'user:urn', type: EntityType.CorpUser, username: 'UserA' },
|
||||
type: OwnershipType.Dataowner,
|
||||
},
|
||||
],
|
||||
lastModified: { time: 1 },
|
||||
},
|
||||
|
||||
@ -34,18 +34,35 @@ export const Ownership: React.FC<Props> = ({ owners, lastModifiedAt, updateOwner
|
||||
|
||||
const ownerTableData = useMemo(
|
||||
() =>
|
||||
stagedOwners.map((owner, index) => ({
|
||||
key: index,
|
||||
urn: owner.owner.urn,
|
||||
ldap: owner.owner.username,
|
||||
fullName: owner.owner.info?.fullName,
|
||||
role: owner.type,
|
||||
pictureLink: owner.owner.editableInfo?.pictureLink,
|
||||
})),
|
||||
// eslint-disable-next-line consistent-return, array-callback-return
|
||||
stagedOwners.map((owner, index) => {
|
||||
if (owner.owner.__typename === 'CorpUser') {
|
||||
return {
|
||||
key: index,
|
||||
urn: owner.owner.urn,
|
||||
ldap: owner.owner.username,
|
||||
fullName: owner.owner.info?.fullName || owner.owner.username,
|
||||
role: owner.type,
|
||||
pictureLink: owner.owner.editableInfo?.pictureLink,
|
||||
type: EntityType.CorpUser,
|
||||
};
|
||||
}
|
||||
if (owner.owner.__typename === 'CorpGroup') {
|
||||
return {
|
||||
key: index,
|
||||
urn: owner.owner.urn,
|
||||
ldap: owner.owner.name,
|
||||
fullName: owner.owner.name,
|
||||
role: owner.type,
|
||||
type: EntityType.CorpGroup,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
[stagedOwners],
|
||||
);
|
||||
|
||||
const isEditing = (record: any) => record.key === editingIndex;
|
||||
const isEditing = (record: { key: number }) => record.key === editingIndex;
|
||||
|
||||
const onAdd = () => {
|
||||
setEditingIndex(stagedOwners.length);
|
||||
@ -53,6 +70,7 @@ export const Ownership: React.FC<Props> = ({ owners, lastModifiedAt, updateOwner
|
||||
form.setFieldsValue({
|
||||
ldap: '',
|
||||
role: OwnershipType.Stakeholder,
|
||||
type: EntityType.CorpUser,
|
||||
});
|
||||
|
||||
const newOwner = {
|
||||
@ -81,26 +99,28 @@ export const Ownership: React.FC<Props> = ({ owners, lastModifiedAt, updateOwner
|
||||
updateOwnership({ owners: updatedOwners });
|
||||
};
|
||||
|
||||
const onChangeOwnerQuery = (query: string) => {
|
||||
getOwnerAutoCompleteResults({
|
||||
variables: {
|
||||
input: {
|
||||
type: EntityType.CorpUser,
|
||||
query,
|
||||
field: 'ldap',
|
||||
const onChangeOwnerQuery = async (query: string) => {
|
||||
if (query && query !== '') {
|
||||
const row = await form.validateFields();
|
||||
getOwnerAutoCompleteResults({
|
||||
variables: {
|
||||
input: {
|
||||
type: row.type,
|
||||
query,
|
||||
field: row.type === EntityType.CorpUser ? 'ldap' : 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
setOwnerQuery(query);
|
||||
};
|
||||
|
||||
const onSave = async (record: any) => {
|
||||
const row = await form.validateFields();
|
||||
|
||||
const updatedOwners = stagedOwners.map((owner, index) => {
|
||||
if (record.key === index) {
|
||||
return {
|
||||
owner: `urn:li:corpuser:${row.ldap}`,
|
||||
owner: `urn:li:${row.type === EntityType.CorpGroup ? 'corpGroup' : 'corpuser'}:${row.ldap}`,
|
||||
type: row.role,
|
||||
};
|
||||
}
|
||||
@ -162,9 +182,10 @@ export const Ownership: React.FC<Props> = ({ owners, lastModifiedAt, updateOwner
|
||||
key={record.urn}
|
||||
placement="left"
|
||||
name={record.fullName}
|
||||
url={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${record.urn}`}
|
||||
url={`/${entityRegistry.getPathName(record.type)}/${record.urn}`}
|
||||
photoUrl={record.pictureLink}
|
||||
style={{ marginRight: '15px' }}
|
||||
isGroup={record.type === EntityType.CorpGroup}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@ -194,7 +215,9 @@ export const Ownership: React.FC<Props> = ({ owners, lastModifiedAt, updateOwner
|
||||
>
|
||||
<Select placeholder="Select a role">
|
||||
{Object.values(OwnershipType).map((value) => (
|
||||
<Select.Option value={value}>{value}</Select.Option>
|
||||
<Select.Option value={value} key={value}>
|
||||
{value}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
@ -203,6 +226,39 @@ export const Ownership: React.FC<Props> = ({ owners, lastModifiedAt, updateOwner
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Type',
|
||||
dataIndex: 'type',
|
||||
render: (type: EntityType, record: any) => {
|
||||
return isEditing(record) ? (
|
||||
<Form.Item
|
||||
name="type"
|
||||
style={{
|
||||
margin: 0,
|
||||
width: '50%',
|
||||
}}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: `Please select a type!`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select placeholder="Select a type" defaultValue={EntityType.CorpUser}>
|
||||
<Select.Option value={EntityType.CorpUser} key={EntityType.CorpUser}>
|
||||
{EntityType.CorpUser}
|
||||
</Select.Option>
|
||||
<Select.Option value={EntityType.CorpGroup} key={EntityType.CorpGroup}>
|
||||
{EntityType.CorpGroup}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Tag>{type}</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'action',
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { grey } from '@ant-design/colors';
|
||||
import { Alert, Avatar, Button, Card, Typography } from 'antd';
|
||||
import { Alert, Button, Card, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import { useHistory, useParams } from 'react-router';
|
||||
import styled from 'styled-components';
|
||||
@ -9,7 +9,7 @@ import { EntityType } from '../../../types.generated';
|
||||
import { useGetAllEntitySearchResults } from '../../../utils/customGraphQL/useGetAllEntitySearchResults';
|
||||
import { navigateToSearchUrl } from '../../search/utils/navigateToSearchUrl';
|
||||
import { Message } from '../../shared/Message';
|
||||
import CustomAvatar from '../../shared/avatar/CustomAvatar';
|
||||
import { AvatarsGroup } from '../../shared/avatar';
|
||||
import { useEntityRegistry } from '../../useEntityRegistry';
|
||||
|
||||
const PageContainer = styled.div`
|
||||
@ -119,19 +119,11 @@ export default function TagProfile() {
|
||||
<div>
|
||||
<CreatedByLabel>Created by</CreatedByLabel>
|
||||
</div>
|
||||
<Avatar.Group maxCount={6} size="large">
|
||||
{data?.tag?.ownership?.owners?.map((owner) => (
|
||||
<div data-testid={`avatar-tag-${owner.owner.urn}`} key={owner.owner.urn}>
|
||||
<CustomAvatar
|
||||
name={owner.owner.info?.fullName || undefined}
|
||||
url={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${
|
||||
owner.owner.urn
|
||||
}`}
|
||||
photoUrl={owner.owner?.editableInfo?.pictureLink || undefined}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
<AvatarsGroup
|
||||
owners={data?.tag?.ownership?.owners}
|
||||
entityRegistry={entityRegistry}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<StatsBox>
|
||||
|
||||
@ -43,13 +43,6 @@ describe('TagProfile', () => {
|
||||
|
||||
expect(getByTestId('avatar-tag-urn:li:corpuser:3')).toBeInTheDocument();
|
||||
expect(getByTestId('avatar-tag-urn:li:corpuser:1')).toBeInTheDocument();
|
||||
|
||||
expect(getByTestId('avatar-tag-urn:li:corpuser:1').querySelector('a').href).toEqual(
|
||||
'http://localhost/user/urn:li:corpuser:1',
|
||||
);
|
||||
expect(getByTestId('avatar-tag-urn:li:corpuser:3').querySelector('a').href).toEqual(
|
||||
'http://localhost/user/urn:li:corpuser:3',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders stats', async () => {
|
||||
|
||||
@ -11,6 +11,7 @@ type Props = {
|
||||
skills?: string[] | null;
|
||||
teams?: string[] | null;
|
||||
email?: string | null;
|
||||
isGroup?: boolean;
|
||||
};
|
||||
|
||||
const Row = styled.div`
|
||||
@ -30,11 +31,16 @@ const Skills = styled.div`
|
||||
margin-right: 32px;
|
||||
`;
|
||||
|
||||
export default function UserHeader({ profileSrc, name, title, skills, teams, email }: Props) {
|
||||
export default function UserHeader({ profileSrc, name, title, skills, teams, email, isGroup = false }: Props) {
|
||||
return (
|
||||
<Row>
|
||||
<AvatarWrapper>
|
||||
<CustomAvatar size={100} photoUrl={profileSrc || undefined} name={name || undefined} />
|
||||
<CustomAvatar
|
||||
size={100}
|
||||
photoUrl={profileSrc || undefined}
|
||||
name={name || undefined}
|
||||
isGroup={isGroup}
|
||||
/>
|
||||
</AvatarWrapper>
|
||||
<div>
|
||||
<Typography.Title level={3}>{name}</Typography.Title>
|
||||
|
||||
55
datahub-web-react/src/app/entity/userGroup/UserGroup.tsx
Normal file
55
datahub-web-react/src/app/entity/userGroup/UserGroup.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import * as React from 'react';
|
||||
import { CorpGroup, EntityType, SearchResult } from '../../../types.generated';
|
||||
import { Entity, IconStyleType, PreviewType } from '../Entity';
|
||||
import { Preview } from './preview/Preview';
|
||||
import UserGroupProfile from './UserGroupProfile';
|
||||
|
||||
/**
|
||||
* Definition of the DataHub CorpGroup entity.
|
||||
*/
|
||||
export class UserGroupEntity implements Entity<CorpGroup> {
|
||||
type: EntityType = EntityType.CorpGroup;
|
||||
|
||||
// TODO: update icons for UserGroup
|
||||
icon = (fontSize: number, styleType: IconStyleType) => {
|
||||
if (styleType === IconStyleType.TAB_VIEW) {
|
||||
return <UserOutlined style={{ fontSize }} />;
|
||||
}
|
||||
|
||||
if (styleType === IconStyleType.HIGHLIGHT) {
|
||||
return <UserOutlined style={{ fontSize, color: 'rgb(144 163 236)' }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<UserOutlined
|
||||
style={{
|
||||
fontSize,
|
||||
color: '#BFBFBF',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
isSearchEnabled = () => false;
|
||||
|
||||
isBrowseEnabled = () => false;
|
||||
|
||||
isLineageEnabled = () => false;
|
||||
|
||||
getAutoCompleteFieldName = () => 'name';
|
||||
|
||||
getPathName: () => string = () => 'userGroup';
|
||||
|
||||
getCollectionName: () => string = () => 'UserGroups';
|
||||
|
||||
renderProfile: (urn: string) => JSX.Element = (_) => <UserGroupProfile />;
|
||||
|
||||
renderPreview = (_: PreviewType, data: CorpGroup) => (
|
||||
<Preview urn={data.urn} name={data.name || data.urn || ''} title={data.name || data.urn || ''} />
|
||||
);
|
||||
|
||||
renderSearch = (result: SearchResult) => {
|
||||
return this.renderPreview(PreviewType.SEARCH, result.entity as CorpGroup);
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
import { Divider, Alert } from 'antd';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import UserHeader from '../user/UserHeader';
|
||||
import useUserParams from '../user/routingUtils/useUserParams';
|
||||
import { useGetUserGroupQuery } from '../../../graphql/user.generated';
|
||||
import { useGetAllEntitySearchResults } from '../../../utils/customGraphQL/useGetAllEntitySearchResults';
|
||||
import { Message } from '../../shared/Message';
|
||||
|
||||
const PageContainer = styled.div`
|
||||
padding: 32px 100px;
|
||||
`;
|
||||
|
||||
const messageStyle = { marginTop: '10%' };
|
||||
|
||||
/**
|
||||
* Responsible for reading & writing users.
|
||||
*/
|
||||
export default function UserGroupProfile() {
|
||||
const { urn } = useUserParams();
|
||||
const { loading, error, data } = useGetUserGroupQuery({ variables: { urn } });
|
||||
|
||||
const name = data?.corpGroup?.name;
|
||||
|
||||
const ownershipResult = useGetAllEntitySearchResults({
|
||||
query: `owners:${name}`,
|
||||
});
|
||||
|
||||
const contentLoading =
|
||||
Object.keys(ownershipResult).some((type) => {
|
||||
return ownershipResult[type].loading;
|
||||
}) || loading;
|
||||
|
||||
if (error || (!loading && !error && !data)) {
|
||||
return <Alert type="error" message={error?.message || 'Entity failed to load'} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{contentLoading && <Message type="loading" content="Loading..." style={messageStyle} />}
|
||||
<UserHeader
|
||||
name={data?.corpGroup?.name}
|
||||
title={data?.corpGroup?.name}
|
||||
email={data?.corpGroup?.info?.email}
|
||||
teams={data?.corpGroup?.info?.groups}
|
||||
isGroup
|
||||
/>
|
||||
<Divider />
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { Space, Typography } from 'antd';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { EntityType } from '../../../../types.generated';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import CustomAvatar from '../../../shared/avatar/CustomAvatar';
|
||||
|
||||
const NameText = styled(Typography.Title)`
|
||||
margin: 0;
|
||||
color: #0073b1;
|
||||
`;
|
||||
const TitleText = styled(Typography.Title)`
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
`;
|
||||
|
||||
export const Preview = ({
|
||||
urn,
|
||||
name,
|
||||
title,
|
||||
photoUrl,
|
||||
}: {
|
||||
urn: string;
|
||||
name: string;
|
||||
title?: string;
|
||||
photoUrl?: string;
|
||||
}): JSX.Element => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
|
||||
return (
|
||||
<Link to={`/${entityRegistry.getPathName(EntityType.CorpGroup)}/${urn}`}>
|
||||
<Space size={28}>
|
||||
<CustomAvatar size={60} photoUrl={photoUrl} name={name} isGroup />
|
||||
<Space direction="vertical" size={4}>
|
||||
<NameText level={3}>{name}</NameText>
|
||||
<TitleText>{title}</TitleText>
|
||||
</Space>
|
||||
</Space>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@ -1,10 +1,10 @@
|
||||
import { Avatar, Divider, Image, Row, Space, Tag, Typography } from 'antd';
|
||||
import { Divider, Image, Row, Space, Tag, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { EntityType, GlobalTags } from '../../types.generated';
|
||||
import { GlobalTags, Owner } from '../../types.generated';
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
import CustomAvatar from '../shared/avatar/CustomAvatar';
|
||||
import AvatarsGroup from '../shared/avatar/AvatarsGroup';
|
||||
import TagGroup from '../shared/tags/TagGroup';
|
||||
|
||||
interface Props {
|
||||
@ -16,7 +16,7 @@ interface Props {
|
||||
platform?: string;
|
||||
qualifier?: string | null;
|
||||
tags?: GlobalTags;
|
||||
owners?: Array<{ urn: string; name?: string; photoUrl?: string }>;
|
||||
owners?: Array<Owner> | null;
|
||||
snippet?: React.ReactNode;
|
||||
}
|
||||
|
||||
@ -56,6 +56,7 @@ export default function DefaultPreviewCard({
|
||||
snippet,
|
||||
}: Props) {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
|
||||
return (
|
||||
<Row style={styles.row} justify="space-between">
|
||||
<Space direction="vertical" align="start" size={28} style={styles.leftColumn}>
|
||||
@ -86,16 +87,7 @@ export default function DefaultPreviewCard({
|
||||
<Space direction="vertical" align="end" size={36} style={styles.rightColumn}>
|
||||
<Space direction="vertical" size={12}>
|
||||
<Typography.Text strong>{owners && owners.length > 0 ? 'Owned By' : ''}</Typography.Text>
|
||||
<Avatar.Group maxCount={4}>
|
||||
{owners?.map((owner) => (
|
||||
<CustomAvatar
|
||||
key={owner.urn}
|
||||
name={owner.name}
|
||||
url={`/${entityRegistry.getPathName(EntityType.CorpUser)}/${owner.urn}`}
|
||||
photoUrl={owner.photoUrl}
|
||||
/>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
<AvatarsGroup owners={owners} entityRegistry={entityRegistry} maxCount={4} />
|
||||
</Space>
|
||||
<TagGroup editableTags={tags} maxShow={3} />
|
||||
</Space>
|
||||
|
||||
41
datahub-web-react/src/app/shared/avatar/AvatarsGroup.tsx
Normal file
41
datahub-web-react/src/app/shared/avatar/AvatarsGroup.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { Avatar } from 'antd';
|
||||
import { AvatarSize } from 'antd/lib/avatar/SizeContext';
|
||||
import React from 'react';
|
||||
import { EntityType, Owner } from '../../../types.generated';
|
||||
import CustomAvatar from './CustomAvatar';
|
||||
import EntityRegistry from '../../entity/EntityRegistry';
|
||||
|
||||
type Props = {
|
||||
owners?: Array<Owner> | null;
|
||||
entityRegistry: EntityRegistry;
|
||||
maxCount?: number;
|
||||
size?: AvatarSize;
|
||||
};
|
||||
|
||||
export default function AvatarsGroup({ owners, entityRegistry, maxCount = 6, size = 'default' }: Props) {
|
||||
return (
|
||||
<Avatar.Group maxCount={maxCount} size={size}>
|
||||
{(owners || [])?.map((owner) => (
|
||||
<div data-testid={`avatar-tag-${owner.owner.urn}`} key={owner.owner.urn}>
|
||||
{owner.owner.__typename === 'CorpUser' ? (
|
||||
<CustomAvatar
|
||||
name={owner.owner.info?.fullName || owner.owner.info?.firstName || owner.owner.info?.email}
|
||||
url={`/${entityRegistry.getPathName(owner.owner.type)}/${owner.owner.urn}`}
|
||||
photoUrl={owner.owner?.editableInfo?.pictureLink || undefined}
|
||||
/>
|
||||
) : (
|
||||
owner.owner.__typename === 'CorpGroup' && (
|
||||
<CustomAvatar
|
||||
name={owner.owner.name || owner.owner.info?.email}
|
||||
url={`/${entityRegistry.getPathName(owner.owner.type || EntityType.CorpGroup)}/${
|
||||
owner.owner.urn
|
||||
}`}
|
||||
isGroup
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Avatar.Group>
|
||||
);
|
||||
}
|
||||
@ -6,9 +6,10 @@ import styled from 'styled-components';
|
||||
|
||||
import defaultAvatar from '../../../images/default_avatar.png';
|
||||
|
||||
const AvatarStyled = styled(Avatar)<{ size?: number }>`
|
||||
const AvatarStyled = styled(Avatar)<{ size?: number; isGroup?: boolean }>`
|
||||
color: #fff;
|
||||
background-color: #ccc;
|
||||
background-color: ${(props) =>
|
||||
props.isGroup ? '#ccc' : '#ccc'}; // TODO: make it different style for corpGroup vs corpUser
|
||||
text-align: center;
|
||||
font-size: ${(props) => (props.size ? `${Math.max(props.size / 2.0, 14)}px` : '14px')} !important;
|
||||
&& > span {
|
||||
@ -24,27 +25,42 @@ type Props = {
|
||||
style?: React.CSSProperties;
|
||||
placement?: TooltipPlacement;
|
||||
size?: number;
|
||||
isGroup?: boolean;
|
||||
};
|
||||
|
||||
export default function CustomAvatar({ url, photoUrl, useDefaultAvatar, name, style, placement, size }: Props) {
|
||||
export default function CustomAvatar({
|
||||
url,
|
||||
photoUrl,
|
||||
useDefaultAvatar,
|
||||
name,
|
||||
style,
|
||||
placement,
|
||||
size,
|
||||
isGroup = false,
|
||||
}: Props) {
|
||||
const avatarWithInitial = name ? (
|
||||
<AvatarStyled style={style} size={size}>
|
||||
<AvatarStyled style={style} size={size} isGroup={isGroup}>
|
||||
{name.charAt(0).toUpperCase()}
|
||||
</AvatarStyled>
|
||||
) : (
|
||||
<AvatarStyled src={defaultAvatar} style={style} size={size} />
|
||||
<AvatarStyled src={defaultAvatar} style={style} size={size} isGroup={isGroup} />
|
||||
);
|
||||
const avatarWithDefault = useDefaultAvatar ? (
|
||||
<AvatarStyled src={defaultAvatar} style={style} size={size} />
|
||||
<AvatarStyled src={defaultAvatar} style={style} size={size} isGroup={isGroup} />
|
||||
) : (
|
||||
avatarWithInitial
|
||||
);
|
||||
const avatar = photoUrl ? <AvatarStyled src={photoUrl} style={style} size={size} /> : avatarWithDefault;
|
||||
const avatar =
|
||||
photoUrl && photoUrl !== '' ? (
|
||||
<AvatarStyled src={photoUrl} style={style} size={size} isGroup={isGroup} />
|
||||
) : (
|
||||
avatarWithDefault
|
||||
);
|
||||
if (!name) {
|
||||
return url ? <Link to={url}>{avatar}</Link> : avatar;
|
||||
}
|
||||
return (
|
||||
<Tooltip title={name} placement={placement}>
|
||||
<Tooltip title={isGroup ? `${name} - Group` : name} placement={placement}>
|
||||
{url ? <Link to={url}>{avatar}</Link> : avatar}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
5
datahub-web-react/src/app/shared/avatar/index.tsx
Normal file
5
datahub-web-react/src/app/shared/avatar/index.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import AvatarsGroupComp from './AvatarsGroup';
|
||||
import CustomAvatarComp from './CustomAvatar';
|
||||
|
||||
export const AvatarsGroup = AvatarsGroupComp;
|
||||
export const CustomAvatar = CustomAvatarComp;
|
||||
@ -1,29 +1,3 @@
|
||||
fragment ownershipFields on Ownership {
|
||||
owners {
|
||||
owner {
|
||||
urn
|
||||
type
|
||||
username
|
||||
info {
|
||||
active
|
||||
displayName
|
||||
title
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
}
|
||||
editableInfo {
|
||||
pictureLink
|
||||
}
|
||||
}
|
||||
type
|
||||
}
|
||||
lastModified {
|
||||
time
|
||||
}
|
||||
}
|
||||
|
||||
fragment globalTagsFields on GlobalTags {
|
||||
tags {
|
||||
tag {
|
||||
@ -34,6 +8,80 @@ fragment globalTagsFields on GlobalTags {
|
||||
}
|
||||
}
|
||||
|
||||
fragment ownershipFields on Ownership {
|
||||
owners {
|
||||
owner {
|
||||
... on CorpUser {
|
||||
urn
|
||||
type
|
||||
username
|
||||
info {
|
||||
active
|
||||
displayName
|
||||
title
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
}
|
||||
editableInfo {
|
||||
pictureLink
|
||||
}
|
||||
}
|
||||
|
||||
... on CorpGroup {
|
||||
urn
|
||||
type
|
||||
name
|
||||
info {
|
||||
email
|
||||
admins {
|
||||
urn
|
||||
username
|
||||
info {
|
||||
active
|
||||
displayName
|
||||
title
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
}
|
||||
editableInfo {
|
||||
pictureLink
|
||||
teams
|
||||
skills
|
||||
}
|
||||
}
|
||||
members {
|
||||
urn
|
||||
username
|
||||
info {
|
||||
active
|
||||
displayName
|
||||
title
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
}
|
||||
editableInfo {
|
||||
pictureLink
|
||||
teams
|
||||
skills
|
||||
}
|
||||
}
|
||||
groups
|
||||
}
|
||||
}
|
||||
}
|
||||
type
|
||||
}
|
||||
lastModified {
|
||||
time
|
||||
}
|
||||
}
|
||||
|
||||
fragment nonRecursiveDatasetFields on Dataset {
|
||||
urn
|
||||
name
|
||||
|
||||
@ -21,3 +21,51 @@ query getUser($urn: String!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query getUserGroup($urn: String!) {
|
||||
corpGroup(urn: $urn) {
|
||||
urn
|
||||
type
|
||||
name
|
||||
info {
|
||||
email
|
||||
admins {
|
||||
urn
|
||||
username
|
||||
info {
|
||||
active
|
||||
displayName
|
||||
title
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
}
|
||||
editableInfo {
|
||||
pictureLink
|
||||
teams
|
||||
skills
|
||||
}
|
||||
}
|
||||
members {
|
||||
urn
|
||||
username
|
||||
info {
|
||||
active
|
||||
displayName
|
||||
title
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
fullName
|
||||
}
|
||||
editableInfo {
|
||||
pictureLink
|
||||
teams
|
||||
skills
|
||||
}
|
||||
}
|
||||
groups
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import { DatasetEntity } from '../../app/entity/dataset/DatasetEntity';
|
||||
import { DataFlowEntity } from '../../app/entity/dataFlow/DataFlowEntity';
|
||||
import { DataJobEntity } from '../../app/entity/dataJob/DataJobEntity';
|
||||
import { UserEntity } from '../../app/entity/user/User';
|
||||
import { UserGroupEntity } from '../../app/entity/userGroup/UserGroup';
|
||||
import EntityRegistry from '../../app/entity/EntityRegistry';
|
||||
import { EntityRegistryContext } from '../../entityRegistryContext';
|
||||
import { TagEntity } from '../../app/entity/tag/Tag';
|
||||
@ -21,6 +22,7 @@ export function getTestEntityRegistry() {
|
||||
const entityRegistry = new EntityRegistry();
|
||||
entityRegistry.register(new DatasetEntity());
|
||||
entityRegistry.register(new UserEntity());
|
||||
entityRegistry.register(new UserGroupEntity());
|
||||
entityRegistry.register(new TagEntity());
|
||||
entityRegistry.register(new DataFlowEntity());
|
||||
entityRegistry.register(new DataJobEntity());
|
||||
|
||||
@ -63,6 +63,44 @@
|
||||
},
|
||||
"proposedDelta": null
|
||||
},
|
||||
{
|
||||
"auditHeader": null,
|
||||
"proposedSnapshot": {
|
||||
"com.linkedin.pegasus2avro.metadata.snapshot.CorpGroupSnapshot": {
|
||||
"urn": "urn:li:corpGroup:jdoe",
|
||||
"aspects": [
|
||||
{
|
||||
"com.linkedin.pegasus2avro.identity.CorpGroupInfo": {
|
||||
"email": "jdoe@linkedin.com",
|
||||
"admins": ["urn:li:corpuser:jdoe", "urn:li:corpuser:datahub"],
|
||||
"members": ["urn:li:corpuser:jdoe", "urn:li:corpuser:datahub"],
|
||||
"groups": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"proposedDelta": null
|
||||
},
|
||||
{
|
||||
"auditHeader": null,
|
||||
"proposedSnapshot": {
|
||||
"com.linkedin.pegasus2avro.metadata.snapshot.CorpGroupSnapshot": {
|
||||
"urn": "urn:li:corpGroup:bfoo",
|
||||
"aspects": [
|
||||
{
|
||||
"com.linkedin.pegasus2avro.identity.CorpGroupInfo": {
|
||||
"email": "bfoo@linkedin.com",
|
||||
"admins": ["urn:li:corpuser:jdoe", "urn:li:corpuser:datahub"],
|
||||
"members": ["urn:li:corpuser:jdoe", "urn:li:corpuser:datahub"],
|
||||
"groups": ["urn:li:corpGroup:jdoe"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"proposedDelta": null
|
||||
},
|
||||
{
|
||||
"auditHeader": null,
|
||||
"proposedSnapshot": {
|
||||
@ -1177,6 +1215,22 @@
|
||||
"com.linkedin.pegasus2avro.metadata.snapshot.DashboardSnapshot": {
|
||||
"urn": "urn:li:dashboard:(looker,baz)",
|
||||
"aspects": [
|
||||
{
|
||||
"com.linkedin.pegasus2avro.common.Ownership": {
|
||||
"owners": [
|
||||
{
|
||||
"owner": "urn:li:corpGroup:bfoo",
|
||||
"type": "DATAOWNER",
|
||||
"source": null
|
||||
}
|
||||
],
|
||||
"lastModified": {
|
||||
"time": 1581407189000,
|
||||
"actor": "urn:li:corpuser:jdoe",
|
||||
"impersonator": null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"com.linkedin.pegasus2avro.dashboard.DashboardInfo": {
|
||||
"title": "Baz Dashboard",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user