mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-14 03:26:47 +00:00
feat(posts): add posts feature to DataHub (#6110)
This commit is contained in:
parent
3c0f63c50a
commit
ee1a1ef45b
@ -72,7 +72,7 @@ play {
|
|||||||
platform {
|
platform {
|
||||||
playVersion = '2.7.6'
|
playVersion = '2.7.6'
|
||||||
scalaVersion = '2.12'
|
scalaVersion = '2.12'
|
||||||
javaVersion = JavaVersion.VERSION_1_8
|
javaVersion = JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
|
|
||||||
injectedRoutesGenerator = true
|
injectedRoutesGenerator = true
|
||||||
@ -81,7 +81,7 @@ play {
|
|||||||
model {
|
model {
|
||||||
components {
|
components {
|
||||||
play {
|
play {
|
||||||
platform play: '2.7.6', scala: '2.12', java: '1.8'
|
platform play: '2.7.6', scala: '2.12', java: '11'
|
||||||
injectedRoutesGenerator = true
|
injectedRoutesGenerator = true
|
||||||
|
|
||||||
binaries.all {
|
binaries.all {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.linkedin.datahub.graphql;
|
|||||||
import com.datahub.authentication.AuthenticationConfiguration;
|
import com.datahub.authentication.AuthenticationConfiguration;
|
||||||
import com.datahub.authentication.group.GroupService;
|
import com.datahub.authentication.group.GroupService;
|
||||||
import com.datahub.authentication.invite.InviteTokenService;
|
import com.datahub.authentication.invite.InviteTokenService;
|
||||||
|
import com.datahub.authentication.post.PostService;
|
||||||
import com.datahub.authentication.token.StatefulTokenService;
|
import com.datahub.authentication.token.StatefulTokenService;
|
||||||
import com.datahub.authentication.user.NativeUserService;
|
import com.datahub.authentication.user.NativeUserService;
|
||||||
import com.datahub.authorization.AuthorizationConfiguration;
|
import com.datahub.authorization.AuthorizationConfiguration;
|
||||||
@ -175,6 +176,8 @@ import com.linkedin.datahub.graphql.resolvers.policy.DeletePolicyResolver;
|
|||||||
import com.linkedin.datahub.graphql.resolvers.policy.GetGrantedPrivilegesResolver;
|
import com.linkedin.datahub.graphql.resolvers.policy.GetGrantedPrivilegesResolver;
|
||||||
import com.linkedin.datahub.graphql.resolvers.policy.ListPoliciesResolver;
|
import com.linkedin.datahub.graphql.resolvers.policy.ListPoliciesResolver;
|
||||||
import com.linkedin.datahub.graphql.resolvers.policy.UpsertPolicyResolver;
|
import com.linkedin.datahub.graphql.resolvers.policy.UpsertPolicyResolver;
|
||||||
|
import com.linkedin.datahub.graphql.resolvers.post.CreatePostResolver;
|
||||||
|
import com.linkedin.datahub.graphql.resolvers.post.ListPostsResolver;
|
||||||
import com.linkedin.datahub.graphql.resolvers.recommendation.ListRecommendationsResolver;
|
import com.linkedin.datahub.graphql.resolvers.recommendation.ListRecommendationsResolver;
|
||||||
import com.linkedin.datahub.graphql.resolvers.role.AcceptRoleResolver;
|
import com.linkedin.datahub.graphql.resolvers.role.AcceptRoleResolver;
|
||||||
import com.linkedin.datahub.graphql.resolvers.role.BatchAssignRoleResolver;
|
import com.linkedin.datahub.graphql.resolvers.role.BatchAssignRoleResolver;
|
||||||
@ -310,6 +313,7 @@ public class GmsGraphQLEngine {
|
|||||||
private final GroupService groupService;
|
private final GroupService groupService;
|
||||||
private final RoleService roleService;
|
private final RoleService roleService;
|
||||||
private final InviteTokenService inviteTokenService;
|
private final InviteTokenService inviteTokenService;
|
||||||
|
private final PostService postService;
|
||||||
|
|
||||||
private final FeatureFlags featureFlags;
|
private final FeatureFlags featureFlags;
|
||||||
|
|
||||||
@ -386,7 +390,7 @@ public class GmsGraphQLEngine {
|
|||||||
final VisualConfiguration visualConfiguration, final TelemetryConfiguration telemetryConfiguration,
|
final VisualConfiguration visualConfiguration, final TelemetryConfiguration telemetryConfiguration,
|
||||||
final TestsConfiguration testsConfiguration, final DatahubConfiguration datahubConfiguration,
|
final TestsConfiguration testsConfiguration, final DatahubConfiguration datahubConfiguration,
|
||||||
final SiblingGraphService siblingGraphService, final GroupService groupService, final RoleService roleService,
|
final SiblingGraphService siblingGraphService, final GroupService groupService, final RoleService roleService,
|
||||||
final InviteTokenService inviteTokenService, final FeatureFlags featureFlags) {
|
final InviteTokenService inviteTokenService, final PostService postService, final FeatureFlags featureFlags) {
|
||||||
|
|
||||||
this.entityClient = entityClient;
|
this.entityClient = entityClient;
|
||||||
this.graphClient = graphClient;
|
this.graphClient = graphClient;
|
||||||
@ -407,6 +411,7 @@ public class GmsGraphQLEngine {
|
|||||||
this.groupService = groupService;
|
this.groupService = groupService;
|
||||||
this.roleService = roleService;
|
this.roleService = roleService;
|
||||||
this.inviteTokenService = inviteTokenService;
|
this.inviteTokenService = inviteTokenService;
|
||||||
|
this.postService = postService;
|
||||||
|
|
||||||
this.ingestionConfiguration = Objects.requireNonNull(ingestionConfiguration);
|
this.ingestionConfiguration = Objects.requireNonNull(ingestionConfiguration);
|
||||||
this.authenticationConfiguration = Objects.requireNonNull(authenticationConfiguration);
|
this.authenticationConfiguration = Objects.requireNonNull(authenticationConfiguration);
|
||||||
@ -676,6 +681,7 @@ public class GmsGraphQLEngine {
|
|||||||
.dataFetcher("entities", getEntitiesResolver())
|
.dataFetcher("entities", getEntitiesResolver())
|
||||||
.dataFetcher("listRoles", new ListRolesResolver(this.entityClient))
|
.dataFetcher("listRoles", new ListRolesResolver(this.entityClient))
|
||||||
.dataFetcher("getInviteToken", new GetInviteTokenResolver(this.inviteTokenService))
|
.dataFetcher("getInviteToken", new GetInviteTokenResolver(this.inviteTokenService))
|
||||||
|
.dataFetcher("listPosts", new ListPostsResolver(this.entityClient))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -798,7 +804,7 @@ public class GmsGraphQLEngine {
|
|||||||
.dataFetcher("batchAssignRole", new BatchAssignRoleResolver(this.roleService))
|
.dataFetcher("batchAssignRole", new BatchAssignRoleResolver(this.roleService))
|
||||||
.dataFetcher("createInviteToken", new CreateInviteTokenResolver(this.inviteTokenService))
|
.dataFetcher("createInviteToken", new CreateInviteTokenResolver(this.inviteTokenService))
|
||||||
.dataFetcher("acceptRole", new AcceptRoleResolver(this.roleService, this.inviteTokenService))
|
.dataFetcher("acceptRole", new AcceptRoleResolver(this.roleService, this.inviteTokenService))
|
||||||
|
.dataFetcher("createPost", new CreatePostResolver(this.postService))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -107,6 +107,10 @@ public class AuthorizationUtils {
|
|||||||
groupUrnStr, orPrivilegeGroups);
|
groupUrnStr, orPrivilegeGroups);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean canCreateGlobalAnnouncements(@Nonnull QueryContext context) {
|
||||||
|
return isAuthorized(context, Optional.empty(), PoliciesConfig.CREATE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isAuthorized(
|
public static boolean isAuthorized(
|
||||||
@Nonnull QueryContext context,
|
@Nonnull QueryContext context,
|
||||||
@Nonnull Optional<ResourceSpec> resourceSpec,
|
@Nonnull Optional<ResourceSpec> resourceSpec,
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
package com.linkedin.datahub.graphql.resolvers.post;
|
||||||
|
|
||||||
|
import com.datahub.authentication.Authentication;
|
||||||
|
import com.datahub.authentication.post.PostService;
|
||||||
|
import com.linkedin.common.Media;
|
||||||
|
import com.linkedin.datahub.graphql.QueryContext;
|
||||||
|
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
|
||||||
|
import com.linkedin.datahub.graphql.exception.AuthorizationException;
|
||||||
|
import com.linkedin.datahub.graphql.generated.CreatePostInput;
|
||||||
|
import com.linkedin.datahub.graphql.generated.PostContentType;
|
||||||
|
import com.linkedin.datahub.graphql.generated.PostType;
|
||||||
|
import com.linkedin.datahub.graphql.generated.UpdateMediaInput;
|
||||||
|
import com.linkedin.datahub.graphql.generated.UpdatePostContentInput;
|
||||||
|
import com.linkedin.post.PostContent;
|
||||||
|
import graphql.schema.DataFetcher;
|
||||||
|
import graphql.schema.DataFetchingEnvironment;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*;
|
||||||
|
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CreatePostResolver implements DataFetcher<CompletableFuture<Boolean>> {
|
||||||
|
private final PostService _postService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Boolean> get(final DataFetchingEnvironment environment) throws Exception {
|
||||||
|
final QueryContext context = environment.getContext();
|
||||||
|
|
||||||
|
if (!AuthorizationUtils.canCreateGlobalAnnouncements(context)) {
|
||||||
|
throw new AuthorizationException(
|
||||||
|
"Unauthorized to create posts. Please contact your DataHub administrator if this needs corrective action.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final CreatePostInput input = bindArgument(environment.getArgument("input"), CreatePostInput.class);
|
||||||
|
final PostType type = input.getPostType();
|
||||||
|
final UpdatePostContentInput content = input.getContent();
|
||||||
|
final PostContentType contentType = content.getContentType();
|
||||||
|
final String title = content.getTitle();
|
||||||
|
final String link = content.getLink();
|
||||||
|
final String description = content.getDescription();
|
||||||
|
final UpdateMediaInput updateMediaInput = content.getMedia();
|
||||||
|
final Authentication authentication = context.getAuthentication();
|
||||||
|
|
||||||
|
Media media = updateMediaInput == null ? null
|
||||||
|
: _postService.mapMedia(updateMediaInput.getType().toString(), updateMediaInput.getLocation());
|
||||||
|
PostContent postContent = _postService.mapPostContent(contentType.toString(), title, description, link, media);
|
||||||
|
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
return _postService.createPost(type.toString(), postContent, authentication);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to create a new post", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
package com.linkedin.datahub.graphql.resolvers.post;
|
||||||
|
|
||||||
|
import com.datahub.authentication.Authentication;
|
||||||
|
import com.linkedin.common.urn.Urn;
|
||||||
|
import com.linkedin.datahub.graphql.QueryContext;
|
||||||
|
import com.linkedin.datahub.graphql.generated.ListPostsInput;
|
||||||
|
import com.linkedin.datahub.graphql.generated.ListPostsResult;
|
||||||
|
import com.linkedin.datahub.graphql.types.post.PostMapper;
|
||||||
|
import com.linkedin.entity.EntityResponse;
|
||||||
|
import com.linkedin.entity.client.EntityClient;
|
||||||
|
import com.linkedin.metadata.query.filter.SortCriterion;
|
||||||
|
import com.linkedin.metadata.query.filter.SortOrder;
|
||||||
|
import com.linkedin.metadata.search.SearchEntity;
|
||||||
|
import com.linkedin.metadata.search.SearchResult;
|
||||||
|
import graphql.schema.DataFetcher;
|
||||||
|
import graphql.schema.DataFetchingEnvironment;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*;
|
||||||
|
import static com.linkedin.metadata.Constants.*;
|
||||||
|
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ListPostsResolver implements DataFetcher<CompletableFuture<ListPostsResult>> {
|
||||||
|
private static final Integer DEFAULT_START = 0;
|
||||||
|
private static final Integer DEFAULT_COUNT = 20;
|
||||||
|
private static final String DEFAULT_QUERY = "";
|
||||||
|
|
||||||
|
private final EntityClient _entityClient;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<ListPostsResult> get(final DataFetchingEnvironment environment) throws Exception {
|
||||||
|
final QueryContext context = environment.getContext();
|
||||||
|
final Authentication authentication = context.getAuthentication();
|
||||||
|
|
||||||
|
final ListPostsInput input = bindArgument(environment.getArgument("input"), ListPostsInput.class);
|
||||||
|
final Integer start = input.getStart() == null ? DEFAULT_START : input.getStart();
|
||||||
|
final Integer count = input.getCount() == null ? DEFAULT_COUNT : input.getCount();
|
||||||
|
final String query = input.getQuery() == null ? DEFAULT_QUERY : input.getQuery();
|
||||||
|
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
final SortCriterion sortCriterion =
|
||||||
|
new SortCriterion().setField(LAST_MODIFIED_FIELD_NAME).setOrder(SortOrder.DESCENDING);
|
||||||
|
|
||||||
|
// First, get all Post Urns.
|
||||||
|
final SearchResult gmsResult = _entityClient.search(POST_ENTITY_NAME, query, null, sortCriterion, start, count,
|
||||||
|
context.getAuthentication());
|
||||||
|
|
||||||
|
// Then, get and hydrate all Posts.
|
||||||
|
final Map<Urn, EntityResponse> entities = _entityClient.batchGetV2(POST_ENTITY_NAME,
|
||||||
|
new HashSet<>(gmsResult.getEntities().stream().map(SearchEntity::getEntity).collect(Collectors.toList())),
|
||||||
|
null, authentication);
|
||||||
|
|
||||||
|
final ListPostsResult result = new ListPostsResult();
|
||||||
|
result.setStart(gmsResult.getFrom());
|
||||||
|
result.setCount(gmsResult.getPageSize());
|
||||||
|
result.setTotal(gmsResult.getNumEntities());
|
||||||
|
result.setPosts(entities.values().stream().map(PostMapper::map).collect(Collectors.toList()));
|
||||||
|
return result;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to list posts", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
package com.linkedin.datahub.graphql.types.post;
|
||||||
|
|
||||||
|
import com.linkedin.data.DataMap;
|
||||||
|
import com.linkedin.datahub.graphql.generated.AuditStamp;
|
||||||
|
import com.linkedin.datahub.graphql.generated.EntityType;
|
||||||
|
import com.linkedin.datahub.graphql.generated.Media;
|
||||||
|
import com.linkedin.datahub.graphql.generated.MediaType;
|
||||||
|
import com.linkedin.datahub.graphql.generated.Post;
|
||||||
|
import com.linkedin.datahub.graphql.generated.PostContent;
|
||||||
|
import com.linkedin.datahub.graphql.generated.PostContentType;
|
||||||
|
import com.linkedin.datahub.graphql.generated.PostType;
|
||||||
|
import com.linkedin.datahub.graphql.types.common.mappers.util.MappingHelper;
|
||||||
|
import com.linkedin.datahub.graphql.types.mappers.ModelMapper;
|
||||||
|
import com.linkedin.entity.EntityResponse;
|
||||||
|
import com.linkedin.entity.EnvelopedAspectMap;
|
||||||
|
import com.linkedin.post.PostInfo;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
import static com.linkedin.metadata.Constants.*;
|
||||||
|
|
||||||
|
|
||||||
|
public class PostMapper implements ModelMapper<EntityResponse, Post> {
|
||||||
|
|
||||||
|
public static final PostMapper INSTANCE = new PostMapper();
|
||||||
|
|
||||||
|
public static Post map(@Nonnull final EntityResponse entityResponse) {
|
||||||
|
return INSTANCE.apply(entityResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Post apply(@Nonnull final EntityResponse entityResponse) {
|
||||||
|
final Post result = new Post();
|
||||||
|
|
||||||
|
result.setUrn(entityResponse.getUrn().toString());
|
||||||
|
result.setType(EntityType.POST);
|
||||||
|
EnvelopedAspectMap aspectMap = entityResponse.getAspects();
|
||||||
|
MappingHelper<Post> mappingHelper = new MappingHelper<>(aspectMap, result);
|
||||||
|
mappingHelper.mapToResult(POST_INFO_ASPECT_NAME, this::mapPostInfo);
|
||||||
|
return mappingHelper.getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mapPostInfo(@Nonnull Post post, @Nonnull DataMap dataMap) {
|
||||||
|
PostInfo postInfo = new PostInfo(dataMap);
|
||||||
|
post.setPostType(PostType.valueOf(postInfo.getType().toString()));
|
||||||
|
post.setContent(mapPostContent(postInfo.getContent()));
|
||||||
|
AuditStamp lastModified = new AuditStamp();
|
||||||
|
lastModified.setTime(postInfo.getLastModified());
|
||||||
|
post.setLastModified(lastModified);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private com.linkedin.datahub.graphql.generated.PostContent mapPostContent(
|
||||||
|
@Nonnull com.linkedin.post.PostContent postContent) {
|
||||||
|
PostContent result = new PostContent();
|
||||||
|
result.setContentType(PostContentType.valueOf(postContent.getType().toString()));
|
||||||
|
result.setTitle(postContent.getTitle());
|
||||||
|
if (postContent.hasDescription()) {
|
||||||
|
result.setDescription(postContent.getDescription());
|
||||||
|
}
|
||||||
|
if (postContent.hasLink()) {
|
||||||
|
result.setLink(postContent.getLink().toString());
|
||||||
|
}
|
||||||
|
if (postContent.hasMedia()) {
|
||||||
|
result.setMedia(mapPostMedia(postContent.getMedia()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private Media mapPostMedia(@Nonnull com.linkedin.common.Media postMedia) {
|
||||||
|
Media result = new Media();
|
||||||
|
result.setType(MediaType.valueOf(postMedia.getType().toString()));
|
||||||
|
result.setLocation(postMedia.getLocation().toString());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -183,6 +183,11 @@ type Query {
|
|||||||
Get invite token
|
Get invite token
|
||||||
"""
|
"""
|
||||||
getInviteToken(input: GetInviteTokenInput!): InviteToken
|
getInviteToken(input: GetInviteTokenInput!): InviteToken
|
||||||
|
|
||||||
|
"""
|
||||||
|
List all Posts
|
||||||
|
"""
|
||||||
|
listPosts(input: ListPostsInput!): ListPostsResult
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -518,6 +523,11 @@ type Mutation {
|
|||||||
Create invite token
|
Create invite token
|
||||||
"""
|
"""
|
||||||
createInviteToken(input: CreateInviteTokenInput!): InviteToken
|
createInviteToken(input: CreateInviteTokenInput!): InviteToken
|
||||||
|
|
||||||
|
"""
|
||||||
|
Create a post
|
||||||
|
"""
|
||||||
|
createPost(input: CreatePostInput!): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -693,6 +703,11 @@ enum EntityType {
|
|||||||
A DataHub Role
|
A DataHub Role
|
||||||
"""
|
"""
|
||||||
DATAHUB_ROLE
|
DATAHUB_ROLE
|
||||||
|
|
||||||
|
"""
|
||||||
|
A DataHub Post
|
||||||
|
"""
|
||||||
|
POST
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -9385,3 +9400,223 @@ input AcceptRoleInput {
|
|||||||
"""
|
"""
|
||||||
inviteToken: String!
|
inviteToken: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
The type of post
|
||||||
|
"""
|
||||||
|
enum PostType {
|
||||||
|
"""
|
||||||
|
Posts on the home page
|
||||||
|
"""
|
||||||
|
HOME_PAGE_ANNOUNCEMENT,
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
The type of post
|
||||||
|
"""
|
||||||
|
enum PostContentType {
|
||||||
|
"""
|
||||||
|
Text content
|
||||||
|
"""
|
||||||
|
TEXT,
|
||||||
|
|
||||||
|
"""
|
||||||
|
Link content
|
||||||
|
"""
|
||||||
|
LINK
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
The type of media
|
||||||
|
"""
|
||||||
|
enum MediaType {
|
||||||
|
"""
|
||||||
|
An image
|
||||||
|
"""
|
||||||
|
IMAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input provided when creating a Post
|
||||||
|
"""
|
||||||
|
input CreatePostInput {
|
||||||
|
"""
|
||||||
|
The type of post
|
||||||
|
"""
|
||||||
|
postType: PostType!
|
||||||
|
|
||||||
|
"""
|
||||||
|
The content of the post
|
||||||
|
"""
|
||||||
|
content: UpdatePostContentInput!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input provided for filling in a post content
|
||||||
|
"""
|
||||||
|
input UpdatePostContentInput {
|
||||||
|
"""
|
||||||
|
The type of post content
|
||||||
|
"""
|
||||||
|
contentType: PostContentType!
|
||||||
|
|
||||||
|
"""
|
||||||
|
The title of the post
|
||||||
|
"""
|
||||||
|
title: String!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Optional content of the post
|
||||||
|
"""
|
||||||
|
description: String
|
||||||
|
|
||||||
|
"""
|
||||||
|
Optional link that the post is associated with
|
||||||
|
"""
|
||||||
|
link: String
|
||||||
|
|
||||||
|
"""
|
||||||
|
Optional media contained in the post
|
||||||
|
"""
|
||||||
|
media: UpdateMediaInput
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input provided for filling in a post content
|
||||||
|
"""
|
||||||
|
input UpdateMediaInput {
|
||||||
|
"""
|
||||||
|
The type of media
|
||||||
|
"""
|
||||||
|
type: MediaType!
|
||||||
|
|
||||||
|
"""
|
||||||
|
The location of the media (a URL)
|
||||||
|
"""
|
||||||
|
location: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input provided when listing existing posts
|
||||||
|
"""
|
||||||
|
input ListPostsInput {
|
||||||
|
"""
|
||||||
|
The starting offset of the result set returned
|
||||||
|
"""
|
||||||
|
start: Int
|
||||||
|
|
||||||
|
"""
|
||||||
|
The maximum number of Roles to be returned in the result set
|
||||||
|
"""
|
||||||
|
count: Int
|
||||||
|
|
||||||
|
"""
|
||||||
|
Optional search query
|
||||||
|
"""
|
||||||
|
query: String
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
The result obtained when listing Posts
|
||||||
|
"""
|
||||||
|
type ListPostsResult {
|
||||||
|
"""
|
||||||
|
The starting offset of the result set returned
|
||||||
|
"""
|
||||||
|
start: Int!
|
||||||
|
|
||||||
|
"""
|
||||||
|
The number of Roles in the returned result set
|
||||||
|
"""
|
||||||
|
count: Int!
|
||||||
|
|
||||||
|
"""
|
||||||
|
The total number of Roles in the result set
|
||||||
|
"""
|
||||||
|
total: Int!
|
||||||
|
|
||||||
|
"""
|
||||||
|
The Posts themselves
|
||||||
|
"""
|
||||||
|
posts: [Post!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Input provided when creating a Post
|
||||||
|
"""
|
||||||
|
type Post implements Entity {
|
||||||
|
"""
|
||||||
|
The primary key of the Post
|
||||||
|
"""
|
||||||
|
urn: String!
|
||||||
|
|
||||||
|
"""
|
||||||
|
The standard Entity Type
|
||||||
|
"""
|
||||||
|
type: EntityType!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Granular API for querying edges extending from the Post
|
||||||
|
"""
|
||||||
|
relationships(input: RelationshipsInput!): EntityRelationshipsResult
|
||||||
|
|
||||||
|
"""
|
||||||
|
The type of post
|
||||||
|
"""
|
||||||
|
postType: PostType!
|
||||||
|
|
||||||
|
"""
|
||||||
|
The content of the post
|
||||||
|
"""
|
||||||
|
content: PostContent!
|
||||||
|
|
||||||
|
"""
|
||||||
|
When the post was last modified
|
||||||
|
"""
|
||||||
|
lastModified: AuditStamp!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Post content
|
||||||
|
"""
|
||||||
|
type PostContent {
|
||||||
|
"""
|
||||||
|
The type of post content
|
||||||
|
"""
|
||||||
|
contentType: PostContentType!
|
||||||
|
|
||||||
|
"""
|
||||||
|
The title of the post
|
||||||
|
"""
|
||||||
|
title: String!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Optional content of the post
|
||||||
|
"""
|
||||||
|
description: String
|
||||||
|
|
||||||
|
"""
|
||||||
|
Optional link that the post is associated with
|
||||||
|
"""
|
||||||
|
link: String
|
||||||
|
|
||||||
|
"""
|
||||||
|
Optional media contained in the post
|
||||||
|
"""
|
||||||
|
media: Media
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Media content
|
||||||
|
"""
|
||||||
|
type Media {
|
||||||
|
"""
|
||||||
|
The type of media
|
||||||
|
"""
|
||||||
|
type: MediaType!
|
||||||
|
|
||||||
|
"""
|
||||||
|
The location of the media (a URL)
|
||||||
|
"""
|
||||||
|
location: String!
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,91 @@
|
|||||||
|
package com.linkedin.datahub.graphql.resolvers.post;
|
||||||
|
|
||||||
|
import com.datahub.authentication.Authentication;
|
||||||
|
import com.datahub.authentication.post.PostService;
|
||||||
|
import com.linkedin.common.Media;
|
||||||
|
import com.linkedin.common.url.Url;
|
||||||
|
import com.linkedin.datahub.graphql.QueryContext;
|
||||||
|
import com.linkedin.datahub.graphql.generated.CreatePostInput;
|
||||||
|
import com.linkedin.datahub.graphql.generated.MediaType;
|
||||||
|
import com.linkedin.datahub.graphql.generated.PostContentType;
|
||||||
|
import com.linkedin.datahub.graphql.generated.PostType;
|
||||||
|
import com.linkedin.datahub.graphql.generated.UpdateMediaInput;
|
||||||
|
import com.linkedin.datahub.graphql.generated.UpdatePostContentInput;
|
||||||
|
import graphql.schema.DataFetchingEnvironment;
|
||||||
|
import org.testng.annotations.BeforeMethod;
|
||||||
|
import org.testng.annotations.Test;
|
||||||
|
|
||||||
|
import static com.linkedin.datahub.graphql.TestUtils.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.testng.Assert.*;
|
||||||
|
|
||||||
|
|
||||||
|
public class CreatePostResolverTest {
|
||||||
|
private static final MediaType POST_MEDIA_TYPE = MediaType.IMAGE;
|
||||||
|
private static final String POST_MEDIA_LOCATION =
|
||||||
|
"https://datahubproject.io/img/datahub-logo-color-light-horizontal.svg";
|
||||||
|
private static final PostContentType POST_CONTENT_TYPE = PostContentType.LINK;
|
||||||
|
private static final String POST_TITLE = "title";
|
||||||
|
private static final String POST_DESCRIPTION = "description";
|
||||||
|
private static final String POST_LINK = "https://datahubproject.io";
|
||||||
|
private PostService _postService;
|
||||||
|
private CreatePostResolver _resolver;
|
||||||
|
private DataFetchingEnvironment _dataFetchingEnvironment;
|
||||||
|
private Authentication _authentication;
|
||||||
|
|
||||||
|
@BeforeMethod
|
||||||
|
public void setupTest() throws Exception {
|
||||||
|
_postService = mock(PostService.class);
|
||||||
|
_dataFetchingEnvironment = mock(DataFetchingEnvironment.class);
|
||||||
|
_authentication = mock(Authentication.class);
|
||||||
|
|
||||||
|
_resolver = new CreatePostResolver(_postService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNotAuthorizedFails() {
|
||||||
|
QueryContext mockContext = getMockDenyContext();
|
||||||
|
when(_dataFetchingEnvironment.getContext()).thenReturn(mockContext);
|
||||||
|
|
||||||
|
assertThrows(() -> _resolver.get(_dataFetchingEnvironment).join());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreatePost() throws Exception {
|
||||||
|
QueryContext mockContext = getMockAllowContext();
|
||||||
|
when(_dataFetchingEnvironment.getContext()).thenReturn(mockContext);
|
||||||
|
when(mockContext.getAuthentication()).thenReturn(_authentication);
|
||||||
|
|
||||||
|
UpdateMediaInput media = new UpdateMediaInput();
|
||||||
|
media.setType(POST_MEDIA_TYPE);
|
||||||
|
media.setLocation(POST_MEDIA_LOCATION);
|
||||||
|
Media mediaObj = new Media().setType(com.linkedin.common.MediaType.valueOf(POST_MEDIA_TYPE.toString()))
|
||||||
|
.setLocation(new Url(POST_MEDIA_LOCATION));
|
||||||
|
when(_postService.mapMedia(eq(POST_MEDIA_TYPE.toString()), eq(POST_MEDIA_LOCATION))).thenReturn(mediaObj);
|
||||||
|
|
||||||
|
UpdatePostContentInput content = new UpdatePostContentInput();
|
||||||
|
content.setTitle(POST_TITLE);
|
||||||
|
content.setDescription(POST_DESCRIPTION);
|
||||||
|
content.setLink(POST_LINK);
|
||||||
|
content.setContentType(POST_CONTENT_TYPE);
|
||||||
|
content.setMedia(media);
|
||||||
|
com.linkedin.post.PostContent postContentObj = new com.linkedin.post.PostContent().setType(
|
||||||
|
com.linkedin.post.PostContentType.valueOf(POST_CONTENT_TYPE.toString()))
|
||||||
|
.setTitle(POST_TITLE)
|
||||||
|
.setDescription(POST_DESCRIPTION)
|
||||||
|
.setLink(new Url(POST_LINK))
|
||||||
|
.setMedia(new Media().setType(com.linkedin.common.MediaType.valueOf(POST_MEDIA_TYPE.toString()))
|
||||||
|
.setLocation(new Url(POST_MEDIA_LOCATION)));
|
||||||
|
when(_postService.mapPostContent(eq(POST_CONTENT_TYPE.toString()), eq(POST_TITLE), eq(POST_DESCRIPTION),
|
||||||
|
eq(POST_LINK), any(Media.class))).thenReturn(postContentObj);
|
||||||
|
|
||||||
|
CreatePostInput input = new CreatePostInput();
|
||||||
|
input.setPostType(PostType.HOME_PAGE_ANNOUNCEMENT);
|
||||||
|
input.setContent(content);
|
||||||
|
when(_dataFetchingEnvironment.getArgument(eq("input"))).thenReturn(input);
|
||||||
|
when(_postService.createPost(eq(PostType.HOME_PAGE_ANNOUNCEMENT.toString()), eq(postContentObj),
|
||||||
|
eq(_authentication))).thenReturn(true);
|
||||||
|
|
||||||
|
assertTrue(_resolver.get(_dataFetchingEnvironment).join());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,120 @@
|
|||||||
|
package com.linkedin.datahub.graphql.resolvers.post;
|
||||||
|
|
||||||
|
import com.datahub.authentication.Authentication;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.linkedin.common.Media;
|
||||||
|
import com.linkedin.common.MediaType;
|
||||||
|
import com.linkedin.common.url.Url;
|
||||||
|
import com.linkedin.common.urn.Urn;
|
||||||
|
import com.linkedin.datahub.graphql.QueryContext;
|
||||||
|
import com.linkedin.datahub.graphql.generated.ListPostsInput;
|
||||||
|
import com.linkedin.datahub.graphql.generated.ListPostsResult;
|
||||||
|
import com.linkedin.entity.Aspect;
|
||||||
|
import com.linkedin.entity.EntityResponse;
|
||||||
|
import com.linkedin.entity.EnvelopedAspect;
|
||||||
|
import com.linkedin.entity.EnvelopedAspectMap;
|
||||||
|
import com.linkedin.entity.client.EntityClient;
|
||||||
|
import com.linkedin.metadata.search.SearchEntity;
|
||||||
|
import com.linkedin.metadata.search.SearchEntityArray;
|
||||||
|
import com.linkedin.metadata.search.SearchResult;
|
||||||
|
import com.linkedin.metadata.search.SearchResultMetadata;
|
||||||
|
import com.linkedin.policy.DataHubRoleInfo;
|
||||||
|
import com.linkedin.post.PostContent;
|
||||||
|
import com.linkedin.post.PostContentType;
|
||||||
|
import com.linkedin.post.PostInfo;
|
||||||
|
import com.linkedin.post.PostType;
|
||||||
|
import graphql.schema.DataFetchingEnvironment;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.testng.annotations.BeforeMethod;
|
||||||
|
import org.testng.annotations.Test;
|
||||||
|
|
||||||
|
import static com.linkedin.datahub.graphql.TestUtils.*;
|
||||||
|
import static com.linkedin.metadata.Constants.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.testng.Assert.*;
|
||||||
|
|
||||||
|
|
||||||
|
public class ListPostsResolverTest {
|
||||||
|
private static Map<Urn, EntityResponse> _entityResponseMap;
|
||||||
|
private static final String POST_URN_STRING = "urn:li:post:examplePost";
|
||||||
|
private static final MediaType POST_MEDIA_TYPE = MediaType.IMAGE;
|
||||||
|
private static final String POST_MEDIA_LOCATION =
|
||||||
|
"https://datahubproject.io/img/datahub-logo-color-light-horizontal.svg";
|
||||||
|
private static final PostContentType POST_CONTENT_TYPE = PostContentType.LINK;
|
||||||
|
private static final String POST_TITLE = "title";
|
||||||
|
private static final String POST_DESCRIPTION = "description";
|
||||||
|
private static final String POST_LINK = "https://datahubproject.io";
|
||||||
|
private static final Media MEDIA = new Media().setType(POST_MEDIA_TYPE).setLocation(new Url(POST_MEDIA_LOCATION));
|
||||||
|
private static final PostContent POST_CONTENT = new PostContent().setType(POST_CONTENT_TYPE)
|
||||||
|
.setTitle(POST_TITLE)
|
||||||
|
.setDescription(POST_DESCRIPTION)
|
||||||
|
.setLink(new Url(POST_LINK))
|
||||||
|
.setMedia(MEDIA);
|
||||||
|
private static final PostType POST_TYPE = PostType.HOME_PAGE_ANNOUNCEMENT;
|
||||||
|
|
||||||
|
private EntityClient _entityClient;
|
||||||
|
private ListPostsResolver _resolver;
|
||||||
|
private DataFetchingEnvironment _dataFetchingEnvironment;
|
||||||
|
private Authentication _authentication;
|
||||||
|
|
||||||
|
private Map<Urn, EntityResponse> getMockPostsEntityResponse() throws URISyntaxException {
|
||||||
|
Urn postUrn = Urn.createFromString(POST_URN_STRING);
|
||||||
|
|
||||||
|
EntityResponse entityResponse = new EntityResponse().setUrn(postUrn);
|
||||||
|
PostInfo postInfo = new PostInfo();
|
||||||
|
postInfo.setType(POST_TYPE);
|
||||||
|
postInfo.setContent(POST_CONTENT);
|
||||||
|
DataHubRoleInfo dataHubRoleInfo = new DataHubRoleInfo();
|
||||||
|
dataHubRoleInfo.setDescription(postUrn.toString());
|
||||||
|
dataHubRoleInfo.setName(postUrn.toString());
|
||||||
|
entityResponse.setAspects(new EnvelopedAspectMap(ImmutableMap.of(DATAHUB_ROLE_INFO_ASPECT_NAME,
|
||||||
|
new EnvelopedAspect().setValue(new Aspect(dataHubRoleInfo.data())))));
|
||||||
|
|
||||||
|
return ImmutableMap.of(postUrn, entityResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeMethod
|
||||||
|
public void setupTest() throws Exception {
|
||||||
|
_entityResponseMap = getMockPostsEntityResponse();
|
||||||
|
|
||||||
|
_entityClient = mock(EntityClient.class);
|
||||||
|
_dataFetchingEnvironment = mock(DataFetchingEnvironment.class);
|
||||||
|
_authentication = mock(Authentication.class);
|
||||||
|
|
||||||
|
_resolver = new ListPostsResolver(_entityClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNotAuthorizedFails() {
|
||||||
|
QueryContext mockContext = getMockDenyContext();
|
||||||
|
when(_dataFetchingEnvironment.getContext()).thenReturn(mockContext);
|
||||||
|
|
||||||
|
assertThrows(() -> _resolver.get(_dataFetchingEnvironment).join());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testListPosts() throws Exception {
|
||||||
|
QueryContext mockContext = getMockAllowContext();
|
||||||
|
when(_dataFetchingEnvironment.getContext()).thenReturn(mockContext);
|
||||||
|
when(mockContext.getAuthentication()).thenReturn(_authentication);
|
||||||
|
|
||||||
|
ListPostsInput input = new ListPostsInput();
|
||||||
|
when(_dataFetchingEnvironment.getArgument("input")).thenReturn(input);
|
||||||
|
final SearchResult roleSearchResult =
|
||||||
|
new SearchResult().setMetadata(new SearchResultMetadata()).setFrom(0).setPageSize(10).setNumEntities(1);
|
||||||
|
roleSearchResult.setEntities(
|
||||||
|
new SearchEntityArray(ImmutableList.of(new SearchEntity().setEntity(Urn.createFromString(POST_URN_STRING)))));
|
||||||
|
|
||||||
|
when(_entityClient.search(eq(POST_ENTITY_NAME), any(), eq(null), any(), anyInt(), anyInt(),
|
||||||
|
eq(_authentication))).thenReturn(roleSearchResult);
|
||||||
|
when(_entityClient.batchGetV2(eq(POST_ENTITY_NAME), any(), any(), any())).thenReturn(_entityResponseMap);
|
||||||
|
|
||||||
|
ListPostsResult result = _resolver.get(_dataFetchingEnvironment).join();
|
||||||
|
assertEquals(result.getStart(), 0);
|
||||||
|
assertEquals(result.getCount(), 10);
|
||||||
|
assertEquals(result.getTotal(), 1);
|
||||||
|
assertEquals(result.getPosts().size(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
datahub-web-react/src/app/home/HomePagePosts.tsx
Normal file
62
datahub-web-react/src/app/home/HomePagePosts.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Divider, Typography } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { useListPostsQuery } from '../../graphql/post.generated';
|
||||||
|
import { Post, PostContentType } from '../../types.generated';
|
||||||
|
import { PostTextCard } from '../search/PostTextCard';
|
||||||
|
import { PostLinkCard } from '../search/PostLinkCard';
|
||||||
|
|
||||||
|
const RecommendationContainer = styled.div`
|
||||||
|
margin-bottom: 32px;
|
||||||
|
max-width: 1000px;
|
||||||
|
min-width: 750px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RecommendationTitle = styled(Typography.Title)`
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ThinDivider = styled(Divider)`
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LinkPostsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const HomePagePosts = () => {
|
||||||
|
const { data: postsData } = useListPostsQuery({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
start: 0,
|
||||||
|
count: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const textPosts =
|
||||||
|
postsData?.listPosts?.posts?.filter((post) => post?.content?.contentType === PostContentType.Text) || [];
|
||||||
|
const linkPosts =
|
||||||
|
postsData?.listPosts?.posts?.filter((post) => post?.content?.contentType === PostContentType.Link) || [];
|
||||||
|
const hasPosts = textPosts.length > 0 || linkPosts.length > 0;
|
||||||
|
return hasPosts ? (
|
||||||
|
<RecommendationContainer>
|
||||||
|
<RecommendationTitle level={4}>Pinned</RecommendationTitle>
|
||||||
|
<ThinDivider />
|
||||||
|
{textPosts.map((post) => (
|
||||||
|
<PostTextCard textPost={post as Post} />
|
||||||
|
))}
|
||||||
|
<LinkPostsContainer>
|
||||||
|
{linkPosts.map((post, index) => (
|
||||||
|
<PostLinkCard linkPost={post as Post} index={index} />
|
||||||
|
))}
|
||||||
|
</LinkPostsContainer>
|
||||||
|
</RecommendationContainer>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components/macro';
|
||||||
import { Button, Divider, Empty, Typography } from 'antd';
|
import { Button, Divider, Empty, Typography } from 'antd';
|
||||||
import { RocketOutlined } from '@ant-design/icons';
|
import { RocketOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
@ -16,6 +16,7 @@ import { useGetEntityCountsQuery } from '../../graphql/app.generated';
|
|||||||
import { GettingStartedModal } from './GettingStartedModal';
|
import { GettingStartedModal } from './GettingStartedModal';
|
||||||
import { ANTD_GRAY } from '../entity/shared/constants';
|
import { ANTD_GRAY } from '../entity/shared/constants';
|
||||||
import { useGetAuthenticatedUser } from '../useGetAuthenticatedUser';
|
import { useGetAuthenticatedUser } from '../useGetAuthenticatedUser';
|
||||||
|
import { HomePagePosts } from './HomePagePosts';
|
||||||
|
|
||||||
const RecommendationsContainer = styled.div`
|
const RecommendationsContainer = styled.div`
|
||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
@ -139,6 +140,7 @@ export const HomePageRecommendations = ({ userUrn }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RecommendationsContainer>
|
<RecommendationsContainer>
|
||||||
|
<HomePagePosts />
|
||||||
{orderedEntityCounts && orderedEntityCounts.length > 0 && (
|
{orderedEntityCounts && orderedEntityCounts.length > 0 && (
|
||||||
<RecommendationContainer>
|
<RecommendationContainer>
|
||||||
{domainRecommendationModule && (
|
{domainRecommendationModule && (
|
||||||
|
|||||||
94
datahub-web-react/src/app/search/PostLinkCard.tsx
Normal file
94
datahub-web-react/src/app/search/PostLinkCard.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import React from 'react';
|
||||||
|
// import { Link } from 'react-router-dom';
|
||||||
|
import { Button, Image, Typography } from 'antd';
|
||||||
|
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||||
|
import styled from 'styled-components/macro';
|
||||||
|
import { ANTD_GRAY } from '../entity/shared/constants';
|
||||||
|
import { Post } from '../../types.generated';
|
||||||
|
|
||||||
|
const CardContainer = styled(Button)<{ isLastCardInRow?: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-right: ${(props) => (props.isLastCardInRow ? '0%' : '4%')};
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
width: 29%;
|
||||||
|
height: 100px;
|
||||||
|
border: 1px solid ${ANTD_GRAY[4]};
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: ${(props) => props.theme.styles['box-shadow']};
|
||||||
|
&&:hover {
|
||||||
|
box-shadow: ${(props) => props.theme.styles['box-shadow-hover']};
|
||||||
|
}
|
||||||
|
white-space: unset;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LogoContainer = styled.div`
|
||||||
|
margin-top: 25px;
|
||||||
|
margin-left: 25px;
|
||||||
|
margin-right: 40px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const PlatformLogo = styled(Image)`
|
||||||
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
background-color: transparent;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TextContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: start;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HeaderText = styled(Typography.Text)`
|
||||||
|
line-height: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TitleDiv = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Title = styled(Typography.Title)`
|
||||||
|
word-break: break-word;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const NUM_CARDS_PER_ROW = 3;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
linkPost: Post;
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PostLinkCard = ({ linkPost, index }: Props) => {
|
||||||
|
const hasMedia = !!linkPost?.content?.media?.location;
|
||||||
|
const link = linkPost?.content?.link || '';
|
||||||
|
const isLastCardInRow = (index + 1) % NUM_CARDS_PER_ROW === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardContainer type="link" href={link} isLastCardInRow={isLastCardInRow}>
|
||||||
|
{hasMedia && (
|
||||||
|
<LogoContainer>
|
||||||
|
<PlatformLogo width={50} height={50} preview={false} src={linkPost?.content?.media?.location} />
|
||||||
|
</LogoContainer>
|
||||||
|
)}
|
||||||
|
<TextContainer>
|
||||||
|
<HeaderText type="secondary">Link</HeaderText>
|
||||||
|
<Title level={5}>
|
||||||
|
<TitleDiv>
|
||||||
|
{linkPost?.content?.title}
|
||||||
|
<ArrowRightOutlined />
|
||||||
|
</TitleDiv>
|
||||||
|
</Title>
|
||||||
|
</TextContainer>
|
||||||
|
</CardContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
65
datahub-web-react/src/app/search/PostTextCard.tsx
Normal file
65
datahub-web-react/src/app/search/PostTextCard.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import styled from 'styled-components/macro';
|
||||||
|
import { ANTD_GRAY } from '../entity/shared/constants';
|
||||||
|
import { Post } from '../../types.generated';
|
||||||
|
|
||||||
|
const CardContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-right: 12px;
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
height: 140px;
|
||||||
|
border: 1px solid ${ANTD_GRAY[4]};
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: ${(props) => props.theme.styles['box-shadow']};
|
||||||
|
&&:hover {
|
||||||
|
box-shadow: ${(props) => props.theme.styles['box-shadow-hover']};
|
||||||
|
}
|
||||||
|
white-space: unset;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TextContainer = styled.div`
|
||||||
|
margin-left: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: start;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Title = styled(Typography.Title)`
|
||||||
|
word-break: break-word;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HeaderText = styled(Typography.Text)`
|
||||||
|
margin-top: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AnnouncementText = styled(Typography.Paragraph)`
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${ANTD_GRAY[7]};
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
textPost: Post;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PostTextCard = ({ textPost }: Props) => {
|
||||||
|
return (
|
||||||
|
<CardContainer>
|
||||||
|
<TextContainer>
|
||||||
|
<HeaderText type="secondary">Announcement</HeaderText>
|
||||||
|
<Title
|
||||||
|
ellipsis={{
|
||||||
|
rows: 1,
|
||||||
|
}}
|
||||||
|
level={5}
|
||||||
|
>
|
||||||
|
{textPost?.content?.title}
|
||||||
|
</Title>
|
||||||
|
<AnnouncementText>{textPost?.content?.description}</AnnouncementText>
|
||||||
|
</TextContainer>
|
||||||
|
</CardContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -114,4 +114,8 @@ mutation createInviteToken($input: CreateInviteTokenInput!) {
|
|||||||
|
|
||||||
mutation acceptRole($input: AcceptRoleInput!) {
|
mutation acceptRole($input: AcceptRoleInput!) {
|
||||||
acceptRole(input: $input)
|
acceptRole(input: $input)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation createPost($input: CreatePostInput!) {
|
||||||
|
createPost(input: $input)
|
||||||
}
|
}
|
||||||
22
datahub-web-react/src/graphql/post.graphql
Normal file
22
datahub-web-react/src/graphql/post.graphql
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
query listPosts($input: ListPostsInput!) {
|
||||||
|
listPosts(input: $input) {
|
||||||
|
start
|
||||||
|
count
|
||||||
|
total
|
||||||
|
posts {
|
||||||
|
urn
|
||||||
|
type
|
||||||
|
postType
|
||||||
|
content {
|
||||||
|
contentType
|
||||||
|
title
|
||||||
|
description
|
||||||
|
link
|
||||||
|
media {
|
||||||
|
type
|
||||||
|
location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -47,6 +47,7 @@ public class Constants {
|
|||||||
public static final String DATA_HUB_UPGRADE_ENTITY_NAME = "dataHubUpgrade";
|
public static final String DATA_HUB_UPGRADE_ENTITY_NAME = "dataHubUpgrade";
|
||||||
public static final String INVITE_TOKEN_ENTITY_NAME = "inviteToken";
|
public static final String INVITE_TOKEN_ENTITY_NAME = "inviteToken";
|
||||||
public static final String DATAHUB_ROLE_ENTITY_NAME = "dataHubRole";
|
public static final String DATAHUB_ROLE_ENTITY_NAME = "dataHubRole";
|
||||||
|
public static final String POST_ENTITY_NAME = "post";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -243,7 +244,6 @@ public class Constants {
|
|||||||
public static final String IS_MEMBER_OF_GROUP_RELATIONSHIP_NAME = "IsMemberOfGroup";
|
public static final String IS_MEMBER_OF_GROUP_RELATIONSHIP_NAME = "IsMemberOfGroup";
|
||||||
public static final String IS_MEMBER_OF_NATIVE_GROUP_RELATIONSHIP_NAME = "IsMemberOfNativeGroup";
|
public static final String IS_MEMBER_OF_NATIVE_GROUP_RELATIONSHIP_NAME = "IsMemberOfNativeGroup";
|
||||||
|
|
||||||
// acryl-main only
|
|
||||||
public static final String CHANGE_EVENT_PLATFORM_EVENT_NAME = "entityChangeEvent";
|
public static final String CHANGE_EVENT_PLATFORM_EVENT_NAME = "entityChangeEvent";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -258,6 +258,10 @@ public class Constants {
|
|||||||
public static final String DATA_PROCESS_INSTANCE_PROPERTIES_ASPECT_NAME = "dataProcessInstanceProperties";
|
public static final String DATA_PROCESS_INSTANCE_PROPERTIES_ASPECT_NAME = "dataProcessInstanceProperties";
|
||||||
public static final String DATA_PROCESS_INSTANCE_RUN_EVENT_ASPECT_NAME = "dataProcessInstanceRunEvent";
|
public static final String DATA_PROCESS_INSTANCE_RUN_EVENT_ASPECT_NAME = "dataProcessInstanceRunEvent";
|
||||||
|
|
||||||
|
// Posts
|
||||||
|
public static final String POST_INFO_ASPECT_NAME = "postInfo";
|
||||||
|
public static final String LAST_MODIFIED_FIELD_NAME = "lastModified";
|
||||||
|
|
||||||
private Constants() {
|
private Constants() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,16 @@
|
|||||||
|
namespace com.linkedin.common
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carries information about which roles a user is assigned to.
|
||||||
|
*/
|
||||||
|
record Media {
|
||||||
|
/**
|
||||||
|
* Type of content the Media is storing, e.g. image, video, etc.
|
||||||
|
*/
|
||||||
|
type: MediaType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where the media content is stored.
|
||||||
|
*/
|
||||||
|
location: Url
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
namespace com.linkedin.common
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum defining the type of content a Media object holds.
|
||||||
|
*/
|
||||||
|
enum MediaType {
|
||||||
|
/**
|
||||||
|
* The Media holds an image.
|
||||||
|
*/
|
||||||
|
IMAGE
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
namespace com.linkedin.metadata.key
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key for a Post.
|
||||||
|
*/
|
||||||
|
@Aspect = {
|
||||||
|
"name": "postKey"
|
||||||
|
}
|
||||||
|
record PostKey {
|
||||||
|
/**
|
||||||
|
* A unique id for the DataHub Post record. Generated on the server side at Post creation time.
|
||||||
|
*/
|
||||||
|
id: string
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
namespace com.linkedin.post
|
||||||
|
|
||||||
|
import com.linkedin.common.Media
|
||||||
|
import com.linkedin.common.Url
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content stored inside a Post.
|
||||||
|
*/
|
||||||
|
record PostContent {
|
||||||
|
/**
|
||||||
|
* Title of the post.
|
||||||
|
*/
|
||||||
|
@Searchable = {
|
||||||
|
"fieldType": "TEXT_PARTIAL"
|
||||||
|
}
|
||||||
|
title: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of content held in the post.
|
||||||
|
*/
|
||||||
|
type: PostContentType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional description of the post.
|
||||||
|
*/
|
||||||
|
description: optional string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional link that the post is associated with.
|
||||||
|
*/
|
||||||
|
link: optional Url
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional media that the post is storing
|
||||||
|
*/
|
||||||
|
media: optional Media
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
namespace com.linkedin.post
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum defining the type of content held in a Post.
|
||||||
|
*/
|
||||||
|
enum PostContentType {
|
||||||
|
/**
|
||||||
|
* Text content
|
||||||
|
*/
|
||||||
|
TEXT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link content
|
||||||
|
*/
|
||||||
|
LINK
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
namespace com.linkedin.post
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about a DataHub Post.
|
||||||
|
*/
|
||||||
|
@Aspect = {
|
||||||
|
"name": "postInfo"
|
||||||
|
}
|
||||||
|
record PostInfo {
|
||||||
|
/**
|
||||||
|
* Type of the Post.
|
||||||
|
*/
|
||||||
|
type: PostType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content stored in the post.
|
||||||
|
*/
|
||||||
|
content: PostContent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time at which the post was initially created
|
||||||
|
*/
|
||||||
|
@Searchable = {
|
||||||
|
"fieldType": "COUNT"
|
||||||
|
}
|
||||||
|
created: long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time at which the post was last modified
|
||||||
|
*/
|
||||||
|
@Searchable = {
|
||||||
|
"fieldType": "COUNT"
|
||||||
|
}
|
||||||
|
lastModified: long
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
namespace com.linkedin.post
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum defining types of Posts.
|
||||||
|
*/
|
||||||
|
enum PostType {
|
||||||
|
/**
|
||||||
|
* The Post is an Home Page announcement.
|
||||||
|
*/
|
||||||
|
HOME_PAGE_ANNOUNCEMENT
|
||||||
|
}
|
||||||
@ -259,4 +259,9 @@ entities:
|
|||||||
keyAspect: dataHubRoleKey
|
keyAspect: dataHubRoleKey
|
||||||
aspects:
|
aspects:
|
||||||
- dataHubRoleInfo
|
- dataHubRoleInfo
|
||||||
|
- name: post
|
||||||
|
category: core
|
||||||
|
keyAspect: postKey
|
||||||
|
aspects:
|
||||||
|
- postInfo
|
||||||
events:
|
events:
|
||||||
|
|||||||
@ -0,0 +1,71 @@
|
|||||||
|
package com.datahub.authentication.post;
|
||||||
|
|
||||||
|
import com.datahub.authentication.Authentication;
|
||||||
|
import com.linkedin.common.Media;
|
||||||
|
import com.linkedin.common.MediaType;
|
||||||
|
import com.linkedin.common.url.Url;
|
||||||
|
import com.linkedin.entity.client.EntityClient;
|
||||||
|
import com.linkedin.metadata.key.PostKey;
|
||||||
|
import com.linkedin.mxe.MetadataChangeProposal;
|
||||||
|
import com.linkedin.post.PostContent;
|
||||||
|
import com.linkedin.post.PostContentType;
|
||||||
|
import com.linkedin.post.PostInfo;
|
||||||
|
import com.linkedin.post.PostType;
|
||||||
|
import com.linkedin.r2.RemoteInvocationException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import static com.linkedin.metadata.Constants.*;
|
||||||
|
import static com.linkedin.metadata.entity.AspectUtils.*;
|
||||||
|
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class PostService {
|
||||||
|
private final EntityClient _entityClient;
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public Media mapMedia(@Nonnull String type, @Nonnull String location) {
|
||||||
|
final Media media = new Media();
|
||||||
|
media.setType(MediaType.valueOf(type));
|
||||||
|
media.setLocation(new Url(location));
|
||||||
|
return media;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public PostContent mapPostContent(@Nonnull String contentType, @Nonnull String title, @Nullable String description, @Nullable String link,
|
||||||
|
@Nullable Media media) {
|
||||||
|
final PostContent postContent = new PostContent().setType(PostContentType.valueOf(contentType)).setTitle(title);
|
||||||
|
if (description != null) {
|
||||||
|
postContent.setDescription(description);
|
||||||
|
}
|
||||||
|
if (link != null) {
|
||||||
|
postContent.setLink(new Url(link));
|
||||||
|
}
|
||||||
|
if (media != null) {
|
||||||
|
postContent.setMedia(media);
|
||||||
|
}
|
||||||
|
return postContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean createPost(@Nonnull String postType, @Nonnull PostContent postContent,
|
||||||
|
@Nonnull Authentication authentication) throws RemoteInvocationException {
|
||||||
|
final String uuid = UUID.randomUUID().toString();
|
||||||
|
final PostKey postKey = new PostKey().setId(uuid);
|
||||||
|
final long currentTimeMillis = Instant.now().toEpochMilli();
|
||||||
|
final PostInfo postInfo = new PostInfo().setType(PostType.valueOf(postType))
|
||||||
|
.setContent(postContent)
|
||||||
|
.setCreated(currentTimeMillis)
|
||||||
|
.setLastModified(currentTimeMillis);
|
||||||
|
|
||||||
|
final MetadataChangeProposal proposal =
|
||||||
|
buildMetadataChangeProposal(POST_ENTITY_NAME, postKey, POST_INFO_ASPECT_NAME, postInfo);
|
||||||
|
_entityClient.ingestProposal(proposal, authentication);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
package com.datahub.authentication.post;
|
||||||
|
|
||||||
|
import com.datahub.authentication.Actor;
|
||||||
|
import com.datahub.authentication.ActorType;
|
||||||
|
import com.datahub.authentication.Authentication;
|
||||||
|
import com.linkedin.common.Media;
|
||||||
|
import com.linkedin.common.MediaType;
|
||||||
|
import com.linkedin.common.url.Url;
|
||||||
|
import com.linkedin.entity.client.EntityClient;
|
||||||
|
import com.linkedin.post.PostContent;
|
||||||
|
import com.linkedin.post.PostContentType;
|
||||||
|
import com.linkedin.post.PostType;
|
||||||
|
import com.linkedin.r2.RemoteInvocationException;
|
||||||
|
import org.testng.annotations.BeforeMethod;
|
||||||
|
import org.testng.annotations.Test;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.testng.Assert.*;
|
||||||
|
|
||||||
|
|
||||||
|
public class PostServiceTest {
|
||||||
|
private static final MediaType POST_MEDIA_TYPE = MediaType.IMAGE;
|
||||||
|
private static final String POST_MEDIA_LOCATION =
|
||||||
|
"https://datahubproject.io/img/datahub-logo-color-light-horizontal.svg";
|
||||||
|
private static final PostContentType POST_CONTENT_TYPE = PostContentType.LINK;
|
||||||
|
private static final String POST_TITLE = "title";
|
||||||
|
private static final String POST_DESCRIPTION = "description";
|
||||||
|
private static final String POST_LINK = "https://datahubproject.io";
|
||||||
|
private static final Media MEDIA = new Media().setType(POST_MEDIA_TYPE).setLocation(new Url(POST_MEDIA_LOCATION));
|
||||||
|
private static final PostContent POST_CONTENT = new PostContent().setType(POST_CONTENT_TYPE)
|
||||||
|
.setTitle(POST_TITLE)
|
||||||
|
.setDescription(POST_DESCRIPTION)
|
||||||
|
.setLink(new Url(POST_LINK))
|
||||||
|
.setMedia(MEDIA);
|
||||||
|
private static final PostType POST_TYPE = PostType.HOME_PAGE_ANNOUNCEMENT;
|
||||||
|
private static final String DATAHUB_SYSTEM_CLIENT_ID = "__datahub_system";
|
||||||
|
private static final Authentication SYSTEM_AUTHENTICATION =
|
||||||
|
new Authentication(new Actor(ActorType.USER, DATAHUB_SYSTEM_CLIENT_ID), "");
|
||||||
|
private EntityClient _entityClient;
|
||||||
|
private PostService _postService;
|
||||||
|
|
||||||
|
@BeforeMethod
|
||||||
|
public void setupTest() {
|
||||||
|
_entityClient = mock(EntityClient.class);
|
||||||
|
_postService = new PostService(_entityClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMapMedia() {
|
||||||
|
Media media = _postService.mapMedia(POST_MEDIA_TYPE.toString(), POST_MEDIA_LOCATION);
|
||||||
|
assertEquals(MEDIA, media);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMapPostContent() {
|
||||||
|
PostContent postContent =
|
||||||
|
_postService.mapPostContent(POST_CONTENT_TYPE.toString(), POST_TITLE, POST_DESCRIPTION, POST_LINK, MEDIA);
|
||||||
|
assertEquals(POST_CONTENT, postContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreatePost() throws RemoteInvocationException {
|
||||||
|
_postService.createPost(POST_TYPE.toString(), POST_CONTENT, SYSTEM_AUTHENTICATION);
|
||||||
|
verify(_entityClient, times(1)).ingestProposal(any(), eq(SYSTEM_AUTHENTICATION));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.linkedin.gms.factory.auth;
|
||||||
|
|
||||||
|
import com.datahub.authentication.post.PostService;
|
||||||
|
import com.linkedin.gms.factory.spring.YamlPropertySourceFactory;
|
||||||
|
import com.linkedin.metadata.client.JavaEntityClient;
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.PropertySource;
|
||||||
|
import org.springframework.context.annotation.Scope;
|
||||||
|
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@PropertySource(value = "classpath:/application.yml", factory = YamlPropertySourceFactory.class)
|
||||||
|
public class PostServiceFactory {
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("javaEntityClient")
|
||||||
|
private JavaEntityClient _javaEntityClient;
|
||||||
|
|
||||||
|
@Bean(name = "postService")
|
||||||
|
@Scope("singleton")
|
||||||
|
@Nonnull
|
||||||
|
protected PostService getInstance() throws Exception {
|
||||||
|
return new PostService(this._javaEntityClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ package com.linkedin.gms.factory.graphql;
|
|||||||
|
|
||||||
import com.datahub.authentication.group.GroupService;
|
import com.datahub.authentication.group.GroupService;
|
||||||
import com.datahub.authentication.invite.InviteTokenService;
|
import com.datahub.authentication.invite.InviteTokenService;
|
||||||
|
import com.datahub.authentication.post.PostService;
|
||||||
import com.datahub.authentication.token.StatefulTokenService;
|
import com.datahub.authentication.token.StatefulTokenService;
|
||||||
import com.datahub.authentication.user.NativeUserService;
|
import com.datahub.authentication.user.NativeUserService;
|
||||||
import com.datahub.authorization.role.RoleService;
|
import com.datahub.authorization.role.RoleService;
|
||||||
@ -123,6 +124,10 @@ public class GraphQLEngineFactory {
|
|||||||
@Qualifier("inviteTokenService")
|
@Qualifier("inviteTokenService")
|
||||||
private InviteTokenService _inviteTokenService;
|
private InviteTokenService _inviteTokenService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("postService")
|
||||||
|
private PostService _postService;
|
||||||
|
|
||||||
@Value("${platformAnalytics.enabled}") // TODO: Migrate to DATAHUB_ANALYTICS_ENABLED
|
@Value("${platformAnalytics.enabled}") // TODO: Migrate to DATAHUB_ANALYTICS_ENABLED
|
||||||
private Boolean isAnalyticsEnabled;
|
private Boolean isAnalyticsEnabled;
|
||||||
|
|
||||||
@ -157,6 +162,7 @@ public class GraphQLEngineFactory {
|
|||||||
_groupService,
|
_groupService,
|
||||||
_roleService,
|
_roleService,
|
||||||
_inviteTokenService,
|
_inviteTokenService,
|
||||||
|
_postService,
|
||||||
_configProvider.getFeatureFlags()
|
_configProvider.getFeatureFlags()
|
||||||
).builder().build();
|
).builder().build();
|
||||||
}
|
}
|
||||||
@ -186,6 +192,7 @@ public class GraphQLEngineFactory {
|
|||||||
_groupService,
|
_groupService,
|
||||||
_roleService,
|
_roleService,
|
||||||
_inviteTokenService,
|
_inviteTokenService,
|
||||||
|
_postService,
|
||||||
_configProvider.getFeatureFlags()
|
_configProvider.getFeatureFlags()
|
||||||
).builder().build();
|
).builder().build();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,6 +93,11 @@ public class PoliciesConfig {
|
|||||||
"Create Domains",
|
"Create Domains",
|
||||||
"Create new Domains.");
|
"Create new Domains.");
|
||||||
|
|
||||||
|
public static final Privilege CREATE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE = Privilege.of(
|
||||||
|
"CREATE_GLOBAL_ANNOUNCEMENTS",
|
||||||
|
"Create Global Announcements",
|
||||||
|
"Create new Global Announcements.");
|
||||||
|
|
||||||
public static final List<Privilege> PLATFORM_PRIVILEGES = ImmutableList.of(
|
public static final List<Privilege> PLATFORM_PRIVILEGES = ImmutableList.of(
|
||||||
MANAGE_POLICIES_PRIVILEGE,
|
MANAGE_POLICIES_PRIVILEGE,
|
||||||
MANAGE_USERS_AND_GROUPS_PRIVILEGE,
|
MANAGE_USERS_AND_GROUPS_PRIVILEGE,
|
||||||
@ -107,7 +112,7 @@ public class PoliciesConfig {
|
|||||||
MANAGE_USER_CREDENTIALS_PRIVILEGE,
|
MANAGE_USER_CREDENTIALS_PRIVILEGE,
|
||||||
MANAGE_TAGS_PRIVILEGE,
|
MANAGE_TAGS_PRIVILEGE,
|
||||||
CREATE_TAGS_PRIVILEGE,
|
CREATE_TAGS_PRIVILEGE,
|
||||||
CREATE_DOMAINS_PRIVILEGE
|
CREATE_DOMAINS_PRIVILEGE, CREATE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE
|
||||||
);
|
);
|
||||||
|
|
||||||
// Resource Privileges //
|
// Resource Privileges //
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user