mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-10 17:46:02 +00:00
feat(groups & owners) Add ability to edit group name + assign creator as owner of metadata (#6047)
Adds ability to edit user group names from the UI. Also sets the creator or Glossary Terms, Glossary Nodes, and Domains to be the owner of said item.
This commit is contained in:
parent
3106e42e89
commit
11092c73cf
@ -767,7 +767,7 @@ public class GmsGraphQLEngine {
|
||||
.dataFetcher("removeUser", new RemoveUserResolver(this.entityClient))
|
||||
.dataFetcher("removeGroup", new RemoveGroupResolver(this.entityClient))
|
||||
.dataFetcher("updateUserStatus", new UpdateUserStatusResolver(this.entityClient))
|
||||
.dataFetcher("createDomain", new CreateDomainResolver(this.entityClient))
|
||||
.dataFetcher("createDomain", new CreateDomainResolver(this.entityClient, this.entityService))
|
||||
.dataFetcher("deleteDomain", new DeleteDomainResolver(entityClient))
|
||||
.dataFetcher("setDomain", new SetDomainResolver(this.entityClient, this.entityService))
|
||||
.dataFetcher("batchSetDomain", new BatchSetDomainResolver(this.entityService))
|
||||
@ -789,8 +789,8 @@ public class GmsGraphQLEngine {
|
||||
.dataFetcher("updateTest", new UpdateTestResolver(this.entityClient))
|
||||
.dataFetcher("deleteTest", new DeleteTestResolver(this.entityClient))
|
||||
.dataFetcher("reportOperation", new ReportOperationResolver(this.entityClient))
|
||||
.dataFetcher("createGlossaryTerm", new CreateGlossaryTermResolver(this.entityClient))
|
||||
.dataFetcher("createGlossaryNode", new CreateGlossaryNodeResolver(this.entityClient))
|
||||
.dataFetcher("createGlossaryTerm", new CreateGlossaryTermResolver(this.entityClient, this.entityService))
|
||||
.dataFetcher("createGlossaryNode", new CreateGlossaryNodeResolver(this.entityClient, this.entityService))
|
||||
.dataFetcher("updateParentNode", new UpdateParentNodeResolver(entityService))
|
||||
.dataFetcher("deleteGlossaryEntity",
|
||||
new DeleteGlossaryEntityResolver(this.entityClient, this.entityService))
|
||||
|
||||
@ -5,9 +5,13 @@ 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.CreateDomainInput;
|
||||
import com.linkedin.datahub.graphql.generated.OwnerEntityType;
|
||||
import com.linkedin.datahub.graphql.generated.OwnershipType;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils;
|
||||
import com.linkedin.domain.DomainProperties;
|
||||
import com.linkedin.entity.client.EntityClient;
|
||||
import com.linkedin.events.metadata.ChangeType;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.key.DomainKey;
|
||||
import com.linkedin.metadata.utils.EntityKeyUtils;
|
||||
@ -30,6 +34,7 @@ import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*;
|
||||
public class CreateDomainResolver implements DataFetcher<CompletableFuture<String>> {
|
||||
|
||||
private final EntityClient _entityClient;
|
||||
private final EntityService _entityService;
|
||||
|
||||
@Override
|
||||
public CompletableFuture<String> get(DataFetchingEnvironment environment) throws Exception {
|
||||
@ -63,7 +68,9 @@ public class CreateDomainResolver implements DataFetcher<CompletableFuture<Strin
|
||||
proposal.setAspect(GenericRecordUtils.serializeAspect(mapDomainProperties(input)));
|
||||
proposal.setChangeType(ChangeType.UPSERT);
|
||||
|
||||
return _entityClient.ingestProposal(proposal, context.getAuthentication());
|
||||
String domainUrn = _entityClient.ingestProposal(proposal, context.getAuthentication());
|
||||
OwnerUtils.addCreatorAsOwner(context, domainUrn, OwnerEntityType.CORP_USER, OwnershipType.TECHNICAL_OWNER, _entityService);
|
||||
return domainUrn;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to create Domain with id: {}, name: {}: {}", input.getId(), input.getName(), e.getMessage());
|
||||
throw new RuntimeException(String.format("Failed to create Domain with id: %s, name: %s", input.getId(), input.getName()), e);
|
||||
|
||||
@ -6,9 +6,13 @@ 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.CreateGlossaryEntityInput;
|
||||
import com.linkedin.datahub.graphql.generated.OwnerEntityType;
|
||||
import com.linkedin.datahub.graphql.generated.OwnershipType;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils;
|
||||
import com.linkedin.entity.client.EntityClient;
|
||||
import com.linkedin.events.metadata.ChangeType;
|
||||
import com.linkedin.glossary.GlossaryNodeInfo;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.key.GlossaryNodeKey;
|
||||
import com.linkedin.metadata.utils.EntityKeyUtils;
|
||||
@ -30,6 +34,7 @@ import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
|
||||
public class CreateGlossaryNodeResolver implements DataFetcher<CompletableFuture<String>> {
|
||||
|
||||
private final EntityClient _entityClient;
|
||||
private final EntityService _entityService;
|
||||
|
||||
@Override
|
||||
public CompletableFuture<String> get(DataFetchingEnvironment environment) throws Exception {
|
||||
@ -56,7 +61,9 @@ public class CreateGlossaryNodeResolver implements DataFetcher<CompletableFuture
|
||||
proposal.setAspect(GenericRecordUtils.serializeAspect(mapGlossaryNodeInfo(input)));
|
||||
proposal.setChangeType(ChangeType.UPSERT);
|
||||
|
||||
return _entityClient.ingestProposal(proposal, context.getAuthentication());
|
||||
String glossaryNodeUrn = _entityClient.ingestProposal(proposal, context.getAuthentication());
|
||||
OwnerUtils.addCreatorAsOwner(context, glossaryNodeUrn, OwnerEntityType.CORP_USER, OwnershipType.TECHNICAL_OWNER, _entityService);
|
||||
return glossaryNodeUrn;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to create GlossaryNode with id: {}, name: {}: {}", input.getId(), input.getName(), e.getMessage());
|
||||
throw new RuntimeException(String.format("Failed to create GlossaryNode with id: %s, name: %s", input.getId(), input.getName()), e);
|
||||
|
||||
@ -6,9 +6,13 @@ 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.CreateGlossaryEntityInput;
|
||||
import com.linkedin.datahub.graphql.generated.OwnerEntityType;
|
||||
import com.linkedin.datahub.graphql.generated.OwnershipType;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils;
|
||||
import com.linkedin.entity.client.EntityClient;
|
||||
import com.linkedin.events.metadata.ChangeType;
|
||||
import com.linkedin.glossary.GlossaryTermInfo;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.key.GlossaryTermKey;
|
||||
import com.linkedin.metadata.utils.EntityKeyUtils;
|
||||
@ -30,6 +34,7 @@ import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
|
||||
public class CreateGlossaryTermResolver implements DataFetcher<CompletableFuture<String>> {
|
||||
|
||||
private final EntityClient _entityClient;
|
||||
private final EntityService _entityService;
|
||||
|
||||
@Override
|
||||
public CompletableFuture<String> get(DataFetchingEnvironment environment) throws Exception {
|
||||
@ -56,7 +61,9 @@ public class CreateGlossaryTermResolver implements DataFetcher<CompletableFuture
|
||||
proposal.setAspect(GenericRecordUtils.serializeAspect(mapGlossaryTermInfo(input)));
|
||||
proposal.setChangeType(ChangeType.UPSERT);
|
||||
|
||||
return _entityClient.ingestProposal(proposal, context.getAuthentication());
|
||||
String glossaryTermUrn = _entityClient.ingestProposal(proposal, context.getAuthentication());
|
||||
OwnerUtils.addCreatorAsOwner(context, glossaryTermUrn, OwnerEntityType.CORP_USER, OwnershipType.TECHNICAL_OWNER, _entityService);
|
||||
return glossaryTermUrn;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to create GlossaryTerm with id: {}, name: {}: {}", input.getId(), input.getName(), e.getMessage());
|
||||
throw new RuntimeException(String.format("Failed to create GlossaryTerm with id: %s, name: %s", input.getId(), input.getName()), e);
|
||||
|
||||
@ -10,6 +10,7 @@ import com.linkedin.datahub.graphql.generated.UpdateNameInput;
|
||||
import com.linkedin.domain.DomainProperties;
|
||||
import com.linkedin.glossary.GlossaryTermInfo;
|
||||
import com.linkedin.glossary.GlossaryNodeInfo;
|
||||
import com.linkedin.identity.CorpGroupInfo;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import graphql.schema.DataFetcher;
|
||||
@ -47,6 +48,8 @@ public class UpdateNameResolver implements DataFetcher<CompletableFuture<Boolean
|
||||
return updateGlossaryNodeName(targetUrn, input, environment.getContext());
|
||||
case Constants.DOMAIN_ENTITY_NAME:
|
||||
return updateDomainName(targetUrn, input, environment.getContext());
|
||||
case Constants.CORP_GROUP_ENTITY_NAME:
|
||||
return updateGroupName(targetUrn, input, environment.getContext());
|
||||
default:
|
||||
throw new RuntimeException(
|
||||
String.format("Failed to update name. Unsupported resource type %s provided.", targetUrn));
|
||||
@ -125,4 +128,28 @@ public class UpdateNameResolver implements DataFetcher<CompletableFuture<Boolean
|
||||
}
|
||||
throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator.");
|
||||
}
|
||||
|
||||
private Boolean updateGroupName(
|
||||
Urn targetUrn,
|
||||
UpdateNameInput input,
|
||||
QueryContext context
|
||||
) {
|
||||
if (AuthorizationUtils.canManageUsersAndGroups(context)) {
|
||||
try {
|
||||
CorpGroupInfo corpGroupInfo = (CorpGroupInfo) getAspectFromEntity(
|
||||
targetUrn.toString(), Constants.CORP_GROUP_INFO_ASPECT_NAME, _entityService, null);
|
||||
if (corpGroupInfo == null) {
|
||||
throw new IllegalArgumentException("Group does not exist");
|
||||
}
|
||||
corpGroupInfo.setDisplayName(input.getName());
|
||||
Urn actor = CorpuserUrn.createFromString(context.getActorUrn());
|
||||
persistAspect(targetUrn, Constants.CORP_GROUP_INFO_ASPECT_NAME, corpGroupInfo, actor, _entityService);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(String.format("Failed to perform update against input %s", input), e);
|
||||
}
|
||||
}
|
||||
throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.mutate.util;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.linkedin.common.urn.CorpuserUrn;
|
||||
|
||||
import com.linkedin.common.Owner;
|
||||
import com.linkedin.common.OwnerArray;
|
||||
@ -34,6 +35,7 @@ import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.*;
|
||||
// TODO: Move to consuming from OwnerService
|
||||
@Slf4j
|
||||
public class OwnerUtils {
|
||||
|
||||
private static final ConjunctivePrivilegeGroup ALL_PRIVILEGES_GROUP = new ConjunctivePrivilegeGroup(ImmutableList.of(
|
||||
PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType()
|
||||
));
|
||||
@ -218,4 +220,23 @@ public class OwnerUtils {
|
||||
entityService.ingestProposal(change, getAuditStamp(actor), false);
|
||||
}
|
||||
}
|
||||
|
||||
public static void addCreatorAsOwner(
|
||||
QueryContext context,
|
||||
String urn,
|
||||
OwnerEntityType ownerEntityType,
|
||||
com.linkedin.datahub.graphql.generated.OwnershipType ownershipType,
|
||||
EntityService entityService) {
|
||||
try {
|
||||
Urn actorUrn = CorpuserUrn.createFromString(context.getActorUrn());
|
||||
addOwnersToResources(
|
||||
ImmutableList.of(new OwnerInput(actorUrn.toString(), ownerEntityType, ownershipType)),
|
||||
ImmutableList.of(new ResourceRefInput(urn, null, null)),
|
||||
actorUrn,
|
||||
entityService
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error(String.format("Failed to add creator as owner of tag %s", urn), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,12 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.tag;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.linkedin.common.urn.CorpuserUrn;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.data.template.SetMode;
|
||||
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.CreateTagInput;
|
||||
import com.linkedin.datahub.graphql.generated.OwnerEntityType;
|
||||
import com.linkedin.datahub.graphql.generated.OwnerInput;
|
||||
import com.linkedin.datahub.graphql.generated.OwnershipType;
|
||||
import com.linkedin.datahub.graphql.generated.ResourceRefInput;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils;
|
||||
import com.linkedin.entity.client.EntityClient;
|
||||
import com.linkedin.events.metadata.ChangeType;
|
||||
@ -74,7 +69,7 @@ public class CreateTagResolver implements DataFetcher<CompletableFuture<String>>
|
||||
proposal.setChangeType(ChangeType.UPSERT);
|
||||
|
||||
String tagUrn = _entityClient.ingestProposal(proposal, context.getAuthentication());
|
||||
addCreatorAsOwner(context, tagUrn);
|
||||
OwnerUtils.addCreatorAsOwner(context, tagUrn, OwnerEntityType.CORP_USER, OwnershipType.TECHNICAL_OWNER, _entityService);
|
||||
return tagUrn;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to create Tag with id: {}, name: {}: {}", input.getId(), input.getName(), e.getMessage());
|
||||
@ -89,18 +84,4 @@ public class CreateTagResolver implements DataFetcher<CompletableFuture<String>>
|
||||
result.setDescription(input.getDescription(), SetMode.IGNORE_NULL);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addCreatorAsOwner(QueryContext context, String tagUrn) {
|
||||
try {
|
||||
Urn actorUrn = CorpuserUrn.createFromString(context.getActorUrn());
|
||||
OwnerUtils.addOwnersToResources(
|
||||
ImmutableList.of(new OwnerInput(actorUrn.toString(), OwnerEntityType.CORP_USER, OwnershipType.TECHNICAL_OWNER)),
|
||||
ImmutableList.of(new ResourceRefInput(tagUrn, null, null)),
|
||||
actorUrn,
|
||||
_entityService
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error(String.format("Failed to add creator as owner of tag %s", tagUrn), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,7 @@ import com.linkedin.events.metadata.ChangeType;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.key.DomainKey;
|
||||
import com.linkedin.metadata.utils.GenericRecordUtils;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.mxe.MetadataChangeProposal;
|
||||
import com.linkedin.r2.RemoteInvocationException;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
@ -27,12 +28,16 @@ public class CreateDomainResolverTest {
|
||||
"test-name",
|
||||
"test-description"
|
||||
);
|
||||
private static final String TEST_ENTITY_URN = "urn:li:dataset:(urn:li:dataPlatform:mysql,my-test,PROD)";
|
||||
private static final String TEST_TAG_1_URN = "urn:li:tag:test-id-1";
|
||||
private static final String TEST_TAG_2_URN = "urn:li:tag:test-id-2";
|
||||
|
||||
@Test
|
||||
public void testGetSuccess() throws Exception {
|
||||
// Create resolver
|
||||
EntityClient mockClient = Mockito.mock(EntityClient.class);
|
||||
CreateDomainResolver resolver = new CreateDomainResolver(mockClient);
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
CreateDomainResolver resolver = new CreateDomainResolver(mockClient, mockService);
|
||||
|
||||
// Execute resolver
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
@ -65,7 +70,8 @@ public class CreateDomainResolverTest {
|
||||
public void testGetUnauthorized() throws Exception {
|
||||
// Create resolver
|
||||
EntityClient mockClient = Mockito.mock(EntityClient.class);
|
||||
CreateDomainResolver resolver = new CreateDomainResolver(mockClient);
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
CreateDomainResolver resolver = new CreateDomainResolver(mockClient, mockService);
|
||||
|
||||
// Execute resolver
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
@ -83,10 +89,11 @@ public class CreateDomainResolverTest {
|
||||
public void testGetEntityClientException() throws Exception {
|
||||
// Create resolver
|
||||
EntityClient mockClient = Mockito.mock(EntityClient.class);
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
Mockito.doThrow(RemoteInvocationException.class).when(mockClient).ingestProposal(
|
||||
Mockito.any(),
|
||||
Mockito.any(Authentication.class));
|
||||
CreateDomainResolver resolver = new CreateDomainResolver(mockClient);
|
||||
CreateDomainResolver resolver = new CreateDomainResolver(mockClient, mockService);
|
||||
|
||||
// Execute resolver
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
|
||||
@ -10,6 +10,7 @@ import com.linkedin.glossary.GlossaryNodeInfo;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.key.GlossaryNodeKey;
|
||||
import com.linkedin.metadata.utils.GenericRecordUtils;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.mxe.MetadataChangeProposal;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import org.mockito.Mockito;
|
||||
@ -74,10 +75,11 @@ public class CreateGlossaryNodeResolverTest {
|
||||
@Test
|
||||
public void testGetSuccess() throws Exception {
|
||||
EntityClient mockClient = Mockito.mock(EntityClient.class);
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
final MetadataChangeProposal proposal = setupTest(mockEnv, TEST_INPUT, "test-description", parentNodeUrn);
|
||||
|
||||
CreateGlossaryNodeResolver resolver = new CreateGlossaryNodeResolver(mockClient);
|
||||
CreateGlossaryNodeResolver resolver = new CreateGlossaryNodeResolver(mockClient, mockService);
|
||||
resolver.get(mockEnv).get();
|
||||
|
||||
Mockito.verify(mockClient, Mockito.times(1)).ingestProposal(
|
||||
@ -89,10 +91,11 @@ public class CreateGlossaryNodeResolverTest {
|
||||
@Test
|
||||
public void testGetSuccessNoDescription() throws Exception {
|
||||
EntityClient mockClient = Mockito.mock(EntityClient.class);
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
final MetadataChangeProposal proposal = setupTest(mockEnv, TEST_INPUT_NO_DESCRIPTION, "", parentNodeUrn);
|
||||
|
||||
CreateGlossaryNodeResolver resolver = new CreateGlossaryNodeResolver(mockClient);
|
||||
CreateGlossaryNodeResolver resolver = new CreateGlossaryNodeResolver(mockClient, mockService);
|
||||
resolver.get(mockEnv).get();
|
||||
|
||||
Mockito.verify(mockClient, Mockito.times(1)).ingestProposal(
|
||||
@ -104,10 +107,11 @@ public class CreateGlossaryNodeResolverTest {
|
||||
@Test
|
||||
public void testGetSuccessNoParentNode() throws Exception {
|
||||
EntityClient mockClient = Mockito.mock(EntityClient.class);
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
final MetadataChangeProposal proposal = setupTest(mockEnv, TEST_INPUT_NO_PARENT_NODE, "test-description", null);
|
||||
|
||||
CreateGlossaryNodeResolver resolver = new CreateGlossaryNodeResolver(mockClient);
|
||||
CreateGlossaryNodeResolver resolver = new CreateGlossaryNodeResolver(mockClient, mockService);
|
||||
resolver.get(mockEnv).get();
|
||||
|
||||
Mockito.verify(mockClient, Mockito.times(1)).ingestProposal(
|
||||
|
||||
@ -10,6 +10,7 @@ import com.linkedin.glossary.GlossaryTermInfo;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.key.GlossaryTermKey;
|
||||
import com.linkedin.metadata.utils.GenericRecordUtils;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.mxe.MetadataChangeProposal;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import org.mockito.Mockito;
|
||||
@ -75,10 +76,11 @@ public class CreateGlossaryTermResolverTest {
|
||||
@Test
|
||||
public void testGetSuccess() throws Exception {
|
||||
EntityClient mockClient = Mockito.mock(EntityClient.class);
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
final MetadataChangeProposal proposal = setupTest(mockEnv, TEST_INPUT, "test-description", parentNodeUrn);
|
||||
|
||||
CreateGlossaryTermResolver resolver = new CreateGlossaryTermResolver(mockClient);
|
||||
CreateGlossaryTermResolver resolver = new CreateGlossaryTermResolver(mockClient, mockService);
|
||||
resolver.get(mockEnv).get();
|
||||
|
||||
Mockito.verify(mockClient, Mockito.times(1)).ingestProposal(
|
||||
@ -90,10 +92,11 @@ public class CreateGlossaryTermResolverTest {
|
||||
@Test
|
||||
public void testGetSuccessNoDescription() throws Exception {
|
||||
EntityClient mockClient = Mockito.mock(EntityClient.class);
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
final MetadataChangeProposal proposal = setupTest(mockEnv, TEST_INPUT_NO_DESCRIPTION, "", parentNodeUrn);
|
||||
|
||||
CreateGlossaryTermResolver resolver = new CreateGlossaryTermResolver(mockClient);
|
||||
CreateGlossaryTermResolver resolver = new CreateGlossaryTermResolver(mockClient, mockService);
|
||||
resolver.get(mockEnv).get();
|
||||
|
||||
Mockito.verify(mockClient, Mockito.times(1)).ingestProposal(
|
||||
@ -105,10 +108,11 @@ public class CreateGlossaryTermResolverTest {
|
||||
@Test
|
||||
public void testGetSuccessNoParentNode() throws Exception {
|
||||
EntityClient mockClient = Mockito.mock(EntityClient.class);
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
final MetadataChangeProposal proposal = setupTest(mockEnv, TEST_INPUT_NO_PARENT_NODE, "test-description", null);
|
||||
|
||||
CreateGlossaryTermResolver resolver = new CreateGlossaryTermResolver(mockClient);
|
||||
CreateGlossaryTermResolver resolver = new CreateGlossaryTermResolver(mockClient, mockService);
|
||||
resolver.get(mockEnv).get();
|
||||
|
||||
Mockito.verify(mockClient, Mockito.times(1)).ingestProposal(
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { Divider, message, Space, Button, Typography, Row, Col, Tooltip } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EditOutlined, LockOutlined, MailOutlined, SlackOutlined } from '@ant-design/icons';
|
||||
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import { useUpdateCorpGroupPropertiesMutation } from '../../../graphql/group.generated';
|
||||
import { EntityRelationshipsResult, Ownership } from '../../../types.generated';
|
||||
import { useUpdateNameMutation } from '../../../graphql/mutations.generated';
|
||||
|
||||
import GroupEditModal from './GroupEditModal';
|
||||
import CustomAvatar from '../../shared/avatar/CustomAvatar';
|
||||
@ -20,6 +21,7 @@ import {
|
||||
GroupsSection,
|
||||
} from '../shared/SidebarStyledComponents';
|
||||
import GroupMembersSideBarSection from './GroupMembersSideBarSection';
|
||||
import { useGetAuthenticatedUser } from '../../useGetAuthenticatedUser';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
@ -34,7 +36,7 @@ type SideBarData = {
|
||||
groupOwnerShip: Ownership;
|
||||
isExternalGroup: boolean;
|
||||
externalGroupType: string | undefined;
|
||||
urn: string | undefined;
|
||||
urn: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@ -61,10 +63,21 @@ const GroupNameHeader = styled(Row)`
|
||||
min-height: 100px;
|
||||
`;
|
||||
|
||||
const GroupName = styled.div`
|
||||
const GroupTitle = styled(Typography.Title)`
|
||||
max-width: 260px;
|
||||
word-wrap: break-word;
|
||||
width: 140px;
|
||||
|
||||
&&& {
|
||||
margin-bottom: 0;
|
||||
word-break: break-all;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.ant-typography-edit {
|
||||
font-size: 16px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
@ -90,7 +103,31 @@ export default function GroupInfoSidebar({ sideBarData, refetch }: Props) {
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
const [editGroupModal, showEditGroupModal] = useState(false);
|
||||
const canEditGroup = true; // TODO; Replace this will fine-grained understanding of user permissions.
|
||||
const me = useGetAuthenticatedUser();
|
||||
const canEditGroup = me?.platformPrivileges.manageIdentities;
|
||||
const [groupTitle, setGroupTitle] = useState(name);
|
||||
const [updateName] = useUpdateNameMutation();
|
||||
|
||||
useEffect(() => {
|
||||
setGroupTitle(groupTitle);
|
||||
}, [groupTitle]);
|
||||
|
||||
// Update Group Title
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const handleTitleUpdate = async (name: string) => {
|
||||
setGroupTitle(name);
|
||||
await updateName({ variables: { input: { name, urn } } })
|
||||
.then(() => {
|
||||
message.success({ content: 'Name Updated', duration: 2 });
|
||||
refetch();
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
message.destroy();
|
||||
if (e instanceof Error) {
|
||||
message.error({ content: `Failed to update name: \n ${e.message || ''}`, duration: 3 });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getEditModalData = {
|
||||
urn,
|
||||
@ -135,7 +172,9 @@ export default function GroupInfoSidebar({ sideBarData, refetch }: Props) {
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<GroupName>{name}</GroupName>
|
||||
<GroupTitle level={3} editable={canEditGroup ? { onChange: handleTitleUpdate } : false}>
|
||||
{groupTitle}
|
||||
</GroupTitle>
|
||||
</Col>
|
||||
<Col>
|
||||
{isExternalGroup && (
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Divider, Popover, Tooltip, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Divider, message, Modal, Popover, Tooltip, Typography } from 'antd';
|
||||
import { blue } from '@ant-design/colors';
|
||||
import styled from 'styled-components';
|
||||
import moment from 'moment';
|
||||
import { Deprecation } from '../../../../../types.generated';
|
||||
import { getLocaleTimezone } from '../../../../shared/time/timeUtils';
|
||||
import { ANTD_GRAY } from '../../constants';
|
||||
import { useBatchUpdateDeprecationMutation } from '../../../../../graphql/mutations.generated';
|
||||
|
||||
const DeprecatedContainer = styled.div`
|
||||
width: 104px;
|
||||
@ -55,12 +57,30 @@ const StyledInfoCircleOutlined = styled(InfoCircleOutlined)`
|
||||
color: #ef5b5b;
|
||||
`;
|
||||
|
||||
const UndeprecatedIcon = styled(InfoCircleOutlined)`
|
||||
font-size: 14px;
|
||||
padding-right: 6px;
|
||||
`;
|
||||
|
||||
const IconGroup = styled.div`
|
||||
font-size: 12px;
|
||||
color: 'black';
|
||||
&:hover {
|
||||
color: ${blue[4]};
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
urn: string;
|
||||
deprecation: Deprecation;
|
||||
preview?: boolean | null;
|
||||
refetch?: () => void;
|
||||
showUndeprecate: boolean | null;
|
||||
};
|
||||
|
||||
export const DeprecationPill = ({ deprecation, preview }: Props) => {
|
||||
export const DeprecationPill = ({ deprecation, preview, urn, refetch, showUndeprecate }: Props) => {
|
||||
const [batchUpdateDeprecationMutation] = useBatchUpdateDeprecationMutation();
|
||||
/**
|
||||
* Deprecation Decommission Timestamp
|
||||
*/
|
||||
@ -78,6 +98,30 @@ export const DeprecationPill = ({ deprecation, preview }: Props) => {
|
||||
const hasDetails = deprecation.note !== '' || deprecation.decommissionTime !== null;
|
||||
const isDividerNeeded = deprecation.note !== '' && deprecation.decommissionTime !== null;
|
||||
|
||||
const batchUndeprecate = () => {
|
||||
batchUpdateDeprecationMutation({
|
||||
variables: {
|
||||
input: {
|
||||
resources: [{ resourceUrn: urn }],
|
||||
deprecated: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ errors }) => {
|
||||
if (!errors) {
|
||||
message.success({ content: 'Marked assets as un-deprecated!', duration: 2 });
|
||||
refetch?.();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
message.destroy();
|
||||
message.error({
|
||||
content: `Failed to mark assets as un-deprecated: \n ${e.message || ''}`,
|
||||
duration: 3,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
overlayStyle={{ maxWidth: 240 }}
|
||||
@ -95,6 +139,27 @@ export const DeprecationPill = ({ deprecation, preview }: Props) => {
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
)}
|
||||
{isDividerNeeded && <ThinDivider />}
|
||||
{showUndeprecate && (
|
||||
<IconGroup
|
||||
onClick={() =>
|
||||
Modal.confirm({
|
||||
title: `Confirm Mark as un-deprecated`,
|
||||
content: `Are you sure you want to mark this asset as un-deprecated?`,
|
||||
onOk() {
|
||||
batchUndeprecate();
|
||||
},
|
||||
onCancel() {},
|
||||
okText: 'Yes',
|
||||
maskClosable: true,
|
||||
closable: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
<UndeprecatedIcon />
|
||||
Mark as un-deprecated
|
||||
</IconGroup>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
'No additional details'
|
||||
|
||||
@ -26,13 +26,16 @@ export default function DeprecationDropdown({ urns, disabled = false, refetch }:
|
||||
})
|
||||
.then(({ errors }) => {
|
||||
if (!errors) {
|
||||
message.success({ content: 'Marked assets as undeprecated!', duration: 2 });
|
||||
message.success({ content: 'Marked assets as un-deprecated!', duration: 2 });
|
||||
refetch?.();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
message.destroy();
|
||||
message.error({ content: `Failed to mark assets as undeprecated: \n ${e.message || ''}`, duration: 3 });
|
||||
message.error({
|
||||
content: `Failed to mark assets as un-deprecated: \n ${e.message || ''}`,
|
||||
duration: 3,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -48,11 +51,11 @@ export default function DeprecationDropdown({ urns, disabled = false, refetch }:
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Mark as undeprecated',
|
||||
title: 'Mark as un-deprecated',
|
||||
onClick: () => {
|
||||
Modal.confirm({
|
||||
title: `Confirm Mark as undeprecated`,
|
||||
content: `Are you sure you want to mark these assets as undeprecated?`,
|
||||
title: `Confirm Mark as un-deprecated`,
|
||||
content: `Are you sure you want to mark these assets as un-deprecated?`,
|
||||
onOk() {
|
||||
batchUndeprecate();
|
||||
},
|
||||
|
||||
@ -125,7 +125,13 @@ export const EntityHeader = ({
|
||||
<TitleWrapper>
|
||||
<EntityName isNameEditable={canEditName} />
|
||||
{entityData?.deprecation?.deprecated && (
|
||||
<DeprecationPill deprecation={entityData?.deprecation} preview={isCompact} />
|
||||
<DeprecationPill
|
||||
urn={urn}
|
||||
deprecation={entityData?.deprecation}
|
||||
showUndeprecate
|
||||
preview={isCompact}
|
||||
refetch={refetch}
|
||||
/>
|
||||
)}
|
||||
{entityData?.health?.map((health) => (
|
||||
<EntityHealthStatus
|
||||
|
||||
@ -288,7 +288,9 @@ export default function DefaultPreviewCard({
|
||||
</EntityTitle>
|
||||
)}
|
||||
</Link>
|
||||
{deprecation?.deprecated && <DeprecationPill deprecation={deprecation} preview />}
|
||||
{deprecation?.deprecated && (
|
||||
<DeprecationPill deprecation={deprecation} urn="" showUndeprecate={false} preview />
|
||||
)}
|
||||
{externalUrl && (
|
||||
<ExternalUrlContainer>
|
||||
<ExternalUrlButton type="link" href={externalUrl} target="_blank">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user