mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-27 18:07:57 +00:00
feat(ui): Batch set & unset Domain for assets via the UI (#5560)
This commit is contained in:
parent
8ea5295294
commit
6c616fd417
@ -150,6 +150,7 @@ import com.linkedin.datahub.graphql.resolvers.mutate.BatchAddTermsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.BatchRemoveOwnersResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.BatchRemoveTagsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.BatchRemoveTermsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.BatchSetDomainResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.MutableTypeResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.RemoveLinkResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.RemoveOwnerResolver;
|
||||
@ -720,6 +721,7 @@ public class GmsGraphQLEngine {
|
||||
.dataFetcher("createDomain", new CreateDomainResolver(this.entityClient))
|
||||
.dataFetcher("deleteDomain", new DeleteDomainResolver(entityClient))
|
||||
.dataFetcher("setDomain", new SetDomainResolver(this.entityClient, this.entityService))
|
||||
.dataFetcher("batchSetDomain", new BatchSetDomainResolver(this.entityService))
|
||||
.dataFetcher("updateDeprecation", new UpdateDeprecationResolver(this.entityClient, this.entityService))
|
||||
.dataFetcher("unsetDomain", new UnsetDomainResolver(this.entityClient, this.entityService))
|
||||
.dataFetcher("createSecret", new CreateSecretResolver(this.entityClient, this.secretService))
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.mutate;
|
||||
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.common.urn.UrnUtils;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.exception.AuthorizationException;
|
||||
import com.linkedin.datahub.graphql.generated.BatchSetDomainInput;
|
||||
import com.linkedin.datahub.graphql.generated.ResourceRefInput;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.util.DomainUtils;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.util.LabelUtils;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nullable;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class BatchSetDomainResolver implements DataFetcher<CompletableFuture<Boolean>> {
|
||||
|
||||
private final EntityService _entityService;
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throws Exception {
|
||||
final QueryContext context = environment.getContext();
|
||||
final BatchSetDomainInput input = bindArgument(environment.getArgument("input"), BatchSetDomainInput.class);
|
||||
final String maybeDomainUrn = input.getDomainUrn();
|
||||
final List<ResourceRefInput> resources = input.getResources();
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
|
||||
// First, validate the domain
|
||||
validateDomain(maybeDomainUrn);
|
||||
validateInputResources(resources, context);
|
||||
|
||||
try {
|
||||
// Then execute the bulk add
|
||||
batchSetDomains(maybeDomainUrn, resources, context);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to perform update against input {}, {}", input.toString(), e.getMessage());
|
||||
throw new RuntimeException(String.format("Failed to perform update against input %s", input.toString()), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void validateDomain(@Nullable String maybeDomainUrn) {
|
||||
if (maybeDomainUrn != null) {
|
||||
DomainUtils.validateDomain(UrnUtils.getUrn(maybeDomainUrn), _entityService);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateInputResources(List<ResourceRefInput> resources, QueryContext context) {
|
||||
for (ResourceRefInput resource : resources) {
|
||||
validateInputResource(resource, context);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateInputResource(ResourceRefInput resource, QueryContext context) {
|
||||
final Urn resourceUrn = UrnUtils.getUrn(resource.getResourceUrn());
|
||||
if (!DomainUtils.isAuthorizedToUpdateDomainsForEntity(context, resourceUrn)) {
|
||||
throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator.");
|
||||
}
|
||||
LabelUtils.validateResource(resourceUrn, resource.getSubResource(), resource.getSubResourceType(), _entityService);
|
||||
}
|
||||
|
||||
private void batchSetDomains(String maybeDomainUrn, List<ResourceRefInput> resources, QueryContext context) {
|
||||
log.debug("Batch adding Domains. domainUrn: {}, resources: {}", maybeDomainUrn, resources);
|
||||
try {
|
||||
DomainUtils.setDomainForResources(maybeDomainUrn == null ? null : UrnUtils.getUrn(maybeDomainUrn),
|
||||
resources,
|
||||
UrnUtils.getUrn(context.getActorUrn()),
|
||||
_entityService);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(String.format("Failed to batch set Domain %s to resources with urns %s!",
|
||||
maybeDomainUrn,
|
||||
resources.stream().map(ResourceRefInput::getResourceUrn).collect(Collectors.toList())),
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,15 +2,27 @@ package com.linkedin.datahub.graphql.resolvers.mutate.util;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import com.linkedin.common.UrnArray;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.common.urn.UrnUtils;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
|
||||
import com.linkedin.datahub.graphql.authorization.ConjunctivePrivilegeGroup;
|
||||
import com.linkedin.datahub.graphql.authorization.DisjunctivePrivilegeGroup;
|
||||
import com.linkedin.datahub.graphql.generated.ResourceRefInput;
|
||||
import com.linkedin.domain.Domains;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.authorization.PoliciesConfig;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.mxe.MetadataChangeProposal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.*;
|
||||
|
||||
|
||||
@Slf4j
|
||||
public class DomainUtils {
|
||||
@ -33,4 +45,49 @@ public class DomainUtils {
|
||||
entityUrn.toString(),
|
||||
orPrivilegeGroups);
|
||||
}
|
||||
|
||||
public static void setDomainForResources(
|
||||
@Nullable Urn domainUrn,
|
||||
List<ResourceRefInput> resources,
|
||||
Urn actor,
|
||||
EntityService entityService
|
||||
) throws Exception {
|
||||
final List<MetadataChangeProposal> changes = new ArrayList<>();
|
||||
for (ResourceRefInput resource : resources) {
|
||||
changes.add(buildSetDomainProposal(domainUrn, resource, actor, entityService));
|
||||
}
|
||||
ingestChangeProposals(changes, entityService, actor);
|
||||
}
|
||||
|
||||
private static MetadataChangeProposal buildSetDomainProposal(
|
||||
@Nullable Urn domainUrn,
|
||||
ResourceRefInput resource,
|
||||
Urn actor,
|
||||
EntityService entityService
|
||||
) {
|
||||
Domains domains = (Domains) getAspectFromEntity(
|
||||
resource.getResourceUrn(),
|
||||
Constants.DOMAINS_ASPECT_NAME,
|
||||
entityService,
|
||||
new Domains());
|
||||
final UrnArray newDomains = new UrnArray();
|
||||
if (domainUrn != null) {
|
||||
newDomains.add(domainUrn);
|
||||
}
|
||||
domains.setDomains(newDomains);
|
||||
return buildMetadataChangeProposal(UrnUtils.getUrn(resource.getResourceUrn()), Constants.DOMAINS_ASPECT_NAME, domains, actor, entityService);
|
||||
}
|
||||
|
||||
public static void validateDomain(Urn domainUrn, EntityService entityService) {
|
||||
if (!entityService.exists(domainUrn)) {
|
||||
throw new IllegalArgumentException(String.format("Failed to validate Domain with urn %s. Urn does not exist.", domainUrn));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ingestChangeProposals(List<MetadataChangeProposal> changes, EntityService entityService, Urn actor) {
|
||||
// TODO: Replace this with a batch ingest proposals endpoint.
|
||||
for (MetadataChangeProposal change : changes) {
|
||||
entityService.ingestProposal(change, getAuditStamp(actor));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -249,6 +249,7 @@ public class LabelUtils {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move this out into a separate utilities class.
|
||||
public static void validateResource(Urn resourceUrn, String subResource, SubResourceType subResourceType, EntityService entityService) {
|
||||
if (!entityService.exists(resourceUrn)) {
|
||||
throw new IllegalArgumentException(String.format("Failed to update resource with urn %s. Entity does not exist.", resourceUrn));
|
||||
|
||||
@ -360,7 +360,6 @@ type Mutation {
|
||||
"""
|
||||
addGroupMembers(input: AddGroupMembersInput!): Boolean
|
||||
|
||||
|
||||
"""
|
||||
Remove members from a group
|
||||
"""
|
||||
@ -390,6 +389,11 @@ type Mutation {
|
||||
"""
|
||||
setDomain(entityUrn: String!, domainUrn: String!): Boolean
|
||||
|
||||
"""
|
||||
Set domain for multiple Entities
|
||||
"""
|
||||
batchSetDomain(input: BatchSetDomainInput!): Boolean
|
||||
|
||||
"""
|
||||
Sets the Domain for a Dataset, Chart, Dashboard, Data Flow (Pipeline), or Data Job (Task). Returns true if the Domain was successfully removed, or was already removed. Requires the Edit Domains privilege for an asset.
|
||||
"""
|
||||
@ -6636,7 +6640,7 @@ input BatchAddTagsInput {
|
||||
"""
|
||||
The target assets to attach the tags to
|
||||
"""
|
||||
resources: [ResourceRefInput]!
|
||||
resources: [ResourceRefInput!]!
|
||||
}
|
||||
|
||||
"""
|
||||
@ -6909,6 +6913,21 @@ input UpdateDeprecationInput {
|
||||
note: String
|
||||
}
|
||||
|
||||
"""
|
||||
Input provided when adding tags to a batch of assets
|
||||
"""
|
||||
input BatchSetDomainInput {
|
||||
"""
|
||||
The primary key of the Domain, or null if the domain will be unset
|
||||
"""
|
||||
domainUrn: String
|
||||
|
||||
"""
|
||||
The target assets to attach the Domain
|
||||
"""
|
||||
resources: [ResourceRefInput!]!
|
||||
}
|
||||
|
||||
"""
|
||||
Input provided when creating or updating an Access Policy
|
||||
"""
|
||||
|
||||
@ -0,0 +1,344 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.domain;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.linkedin.common.AuditStamp;
|
||||
import com.linkedin.common.UrnArray;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.common.urn.UrnUtils;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.generated.BatchSetDomainInput;
|
||||
import com.linkedin.datahub.graphql.generated.ResourceRefInput;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.BatchSetDomainResolver;
|
||||
import com.linkedin.domain.Domains;
|
||||
import com.linkedin.events.metadata.ChangeType;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.metadata.utils.GenericRecordUtils;
|
||||
import com.linkedin.mxe.MetadataChangeProposal;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import org.mockito.Mockito;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import static com.linkedin.datahub.graphql.TestUtils.*;
|
||||
import static org.testng.Assert.*;
|
||||
|
||||
|
||||
public class BatchSetDomainResolverTest {
|
||||
|
||||
private static final String TEST_ENTITY_URN_1 = "urn:li:dataset:(urn:li:dataPlatform:mysql,my-test,PROD)";
|
||||
private static final String TEST_ENTITY_URN_2 = "urn:li:dataset:(urn:li:dataPlatform:mysql,my-test-2,PROD)";
|
||||
private static final String TEST_DOMAIN_1_URN = "urn:li:domain:test-id-1";
|
||||
private static final String TEST_DOMAIN_2_URN = "urn:li:domain:test-id-2";
|
||||
|
||||
@Test
|
||||
public void testGetSuccessNoExistingDomains() throws Exception {
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
|
||||
Mockito.when(mockService.getAspect(
|
||||
Mockito.eq(UrnUtils.getUrn(TEST_ENTITY_URN_1)),
|
||||
Mockito.eq(Constants.DOMAINS_ASPECT_NAME),
|
||||
Mockito.eq(0L)))
|
||||
.thenReturn(null);
|
||||
|
||||
Mockito.when(mockService.getAspect(
|
||||
Mockito.eq(UrnUtils.getUrn(TEST_ENTITY_URN_2)),
|
||||
Mockito.eq(Constants.DOMAINS_ASPECT_NAME),
|
||||
Mockito.eq(0L)))
|
||||
.thenReturn(null);
|
||||
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN_1))).thenReturn(true);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN_2))).thenReturn(true);
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_DOMAIN_1_URN))).thenReturn(true);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_DOMAIN_2_URN))).thenReturn(true);
|
||||
|
||||
BatchSetDomainResolver resolver = new BatchSetDomainResolver(mockService);
|
||||
|
||||
// Execute resolver
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
BatchSetDomainInput input = new BatchSetDomainInput(TEST_DOMAIN_2_URN, ImmutableList.of(
|
||||
new ResourceRefInput(TEST_ENTITY_URN_1, null, null),
|
||||
new ResourceRefInput(TEST_ENTITY_URN_2, null, null)));
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
assertTrue(resolver.get(mockEnv).get());
|
||||
|
||||
final Domains newDomains = new Domains().setDomains(new UrnArray(ImmutableList.of(
|
||||
Urn.createFromString(TEST_DOMAIN_2_URN)
|
||||
)));
|
||||
|
||||
final MetadataChangeProposal proposal1 = new MetadataChangeProposal();
|
||||
proposal1.setEntityUrn(Urn.createFromString(TEST_ENTITY_URN_1));
|
||||
proposal1.setEntityType(Constants.DATASET_ENTITY_NAME);
|
||||
proposal1.setAspectName(Constants.DOMAINS_ASPECT_NAME);
|
||||
proposal1.setAspect(GenericRecordUtils.serializeAspect(newDomains));
|
||||
proposal1.setChangeType(ChangeType.UPSERT);
|
||||
|
||||
Mockito.verify(mockService, Mockito.times(1)).ingestProposal(
|
||||
Mockito.eq(proposal1),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
|
||||
final MetadataChangeProposal proposal2 = new MetadataChangeProposal();
|
||||
proposal2.setEntityUrn(Urn.createFromString(TEST_ENTITY_URN_2));
|
||||
proposal2.setEntityType(Constants.DATASET_ENTITY_NAME);
|
||||
proposal2.setAspectName(Constants.DOMAINS_ASPECT_NAME);
|
||||
proposal2.setAspect(GenericRecordUtils.serializeAspect(newDomains));
|
||||
proposal2.setChangeType(ChangeType.UPSERT);
|
||||
|
||||
Mockito.verify(mockService, Mockito.times(1)).ingestProposal(
|
||||
Mockito.eq(proposal2),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
|
||||
Mockito.verify(mockService, Mockito.times(1)).exists(
|
||||
Mockito.eq(Urn.createFromString(TEST_DOMAIN_2_URN))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuccessExistingDomains() throws Exception {
|
||||
final Domains originalDomain = new Domains().setDomains(new UrnArray(ImmutableList.of(
|
||||
Urn.createFromString(TEST_DOMAIN_1_URN))));
|
||||
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
|
||||
Mockito.when(mockService.getAspect(
|
||||
Mockito.eq(UrnUtils.getUrn(TEST_ENTITY_URN_1)),
|
||||
Mockito.eq(Constants.DOMAINS_ASPECT_NAME),
|
||||
Mockito.eq(0L)))
|
||||
.thenReturn(originalDomain);
|
||||
|
||||
Mockito.when(mockService.getAspect(
|
||||
Mockito.eq(UrnUtils.getUrn(TEST_ENTITY_URN_2)),
|
||||
Mockito.eq(Constants.DOMAINS_ASPECT_NAME),
|
||||
Mockito.eq(0L)))
|
||||
.thenReturn(originalDomain);
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN_1))).thenReturn(true);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN_2))).thenReturn(true);
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_DOMAIN_1_URN))).thenReturn(true);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_DOMAIN_2_URN))).thenReturn(true);
|
||||
|
||||
BatchSetDomainResolver resolver = new BatchSetDomainResolver(mockService);
|
||||
|
||||
// Execute resolver
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
BatchSetDomainInput input = new BatchSetDomainInput(TEST_DOMAIN_2_URN, ImmutableList.of(
|
||||
new ResourceRefInput(TEST_ENTITY_URN_1, null, null),
|
||||
new ResourceRefInput(TEST_ENTITY_URN_2, null, null)));
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
assertTrue(resolver.get(mockEnv).get());
|
||||
|
||||
final Domains newDomains = new Domains().setDomains(new UrnArray(ImmutableList.of(
|
||||
Urn.createFromString(TEST_DOMAIN_2_URN)
|
||||
)));
|
||||
|
||||
final MetadataChangeProposal proposal1 = new MetadataChangeProposal();
|
||||
proposal1.setEntityUrn(Urn.createFromString(TEST_ENTITY_URN_1));
|
||||
proposal1.setEntityType(Constants.DATASET_ENTITY_NAME);
|
||||
proposal1.setAspectName(Constants.DOMAINS_ASPECT_NAME);
|
||||
proposal1.setAspect(GenericRecordUtils.serializeAspect(newDomains));
|
||||
proposal1.setChangeType(ChangeType.UPSERT);
|
||||
|
||||
Mockito.verify(mockService, Mockito.times(1)).ingestProposal(
|
||||
Mockito.eq(proposal1),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
|
||||
final MetadataChangeProposal proposal2 = new MetadataChangeProposal();
|
||||
proposal2.setEntityUrn(Urn.createFromString(TEST_ENTITY_URN_2));
|
||||
proposal2.setEntityType(Constants.DATASET_ENTITY_NAME);
|
||||
proposal2.setAspectName(Constants.DOMAINS_ASPECT_NAME);
|
||||
proposal2.setAspect(GenericRecordUtils.serializeAspect(newDomains));
|
||||
proposal2.setChangeType(ChangeType.UPSERT);
|
||||
|
||||
Mockito.verify(mockService, Mockito.times(1)).ingestProposal(
|
||||
Mockito.eq(proposal2),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
|
||||
Mockito.verify(mockService, Mockito.times(1)).exists(
|
||||
Mockito.eq(Urn.createFromString(TEST_DOMAIN_2_URN))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuccessUnsetDomains() throws Exception {
|
||||
final Domains originalDomain = new Domains().setDomains(new UrnArray(ImmutableList.of(
|
||||
Urn.createFromString(TEST_DOMAIN_1_URN))));
|
||||
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
|
||||
Mockito.when(mockService.getAspect(
|
||||
Mockito.eq(UrnUtils.getUrn(TEST_ENTITY_URN_1)),
|
||||
Mockito.eq(Constants.DOMAINS_ASPECT_NAME),
|
||||
Mockito.eq(0L)))
|
||||
.thenReturn(originalDomain);
|
||||
|
||||
Mockito.when(mockService.getAspect(
|
||||
Mockito.eq(UrnUtils.getUrn(TEST_ENTITY_URN_2)),
|
||||
Mockito.eq(Constants.DOMAINS_ASPECT_NAME),
|
||||
Mockito.eq(0L)))
|
||||
.thenReturn(originalDomain);
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN_1))).thenReturn(true);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN_2))).thenReturn(true);
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_DOMAIN_1_URN))).thenReturn(true);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_DOMAIN_2_URN))).thenReturn(true);
|
||||
|
||||
BatchSetDomainResolver resolver = new BatchSetDomainResolver(mockService);
|
||||
|
||||
// Execute resolver
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
BatchSetDomainInput input = new BatchSetDomainInput(null, ImmutableList.of(
|
||||
new ResourceRefInput(TEST_ENTITY_URN_1, null, null),
|
||||
new ResourceRefInput(TEST_ENTITY_URN_2, null, null)));
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
assertTrue(resolver.get(mockEnv).get());
|
||||
|
||||
final Domains newDomains = new Domains().setDomains(new UrnArray(ImmutableList.of()));
|
||||
|
||||
final MetadataChangeProposal proposal1 = new MetadataChangeProposal();
|
||||
proposal1.setEntityUrn(Urn.createFromString(TEST_ENTITY_URN_1));
|
||||
proposal1.setEntityType(Constants.DATASET_ENTITY_NAME);
|
||||
proposal1.setAspectName(Constants.DOMAINS_ASPECT_NAME);
|
||||
proposal1.setAspect(GenericRecordUtils.serializeAspect(newDomains));
|
||||
proposal1.setChangeType(ChangeType.UPSERT);
|
||||
|
||||
Mockito.verify(mockService, Mockito.times(1)).ingestProposal(
|
||||
Mockito.eq(proposal1),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
|
||||
final MetadataChangeProposal proposal2 = new MetadataChangeProposal();
|
||||
proposal2.setEntityUrn(Urn.createFromString(TEST_ENTITY_URN_2));
|
||||
proposal2.setEntityType(Constants.DATASET_ENTITY_NAME);
|
||||
proposal2.setAspectName(Constants.DOMAINS_ASPECT_NAME);
|
||||
proposal2.setAspect(GenericRecordUtils.serializeAspect(newDomains));
|
||||
proposal2.setChangeType(ChangeType.UPSERT);
|
||||
|
||||
Mockito.verify(mockService, Mockito.times(1)).ingestProposal(
|
||||
Mockito.eq(proposal2),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailureDomainDoesNotExist() throws Exception {
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
|
||||
Mockito.when(mockService.getAspect(
|
||||
Mockito.eq(UrnUtils.getUrn(TEST_ENTITY_URN_1)),
|
||||
Mockito.eq(Constants.DOMAINS_ASPECT_NAME),
|
||||
Mockito.eq(0L)))
|
||||
.thenReturn(null);
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN_1))).thenReturn(true);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_DOMAIN_1_URN))).thenReturn(false);
|
||||
|
||||
BatchSetDomainResolver resolver = new BatchSetDomainResolver(mockService);
|
||||
|
||||
// Execute resolver
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
BatchSetDomainInput input = new BatchSetDomainInput(null, ImmutableList.of(
|
||||
new ResourceRefInput(TEST_ENTITY_URN_1, null, null),
|
||||
new ResourceRefInput(TEST_ENTITY_URN_2, null, null)));
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
|
||||
Mockito.verify(mockService, Mockito.times(0)).ingestProposal(
|
||||
Mockito.any(),
|
||||
Mockito.any(AuditStamp.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailureResourceDoesNotExist() throws Exception {
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
|
||||
Mockito.when(mockService.getAspect(
|
||||
Mockito.eq(UrnUtils.getUrn(TEST_ENTITY_URN_1)),
|
||||
Mockito.eq(Constants.DOMAINS_ASPECT_NAME),
|
||||
Mockito.eq(0L)))
|
||||
.thenReturn(null);
|
||||
Mockito.when(mockService.getAspect(
|
||||
Mockito.eq(UrnUtils.getUrn(TEST_ENTITY_URN_2)),
|
||||
Mockito.eq(Constants.DOMAINS_ASPECT_NAME),
|
||||
Mockito.eq(0L)))
|
||||
.thenReturn(null);
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN_1))).thenReturn(false);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN_2))).thenReturn(true);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_DOMAIN_1_URN))).thenReturn(true);
|
||||
|
||||
BatchSetDomainResolver resolver = new BatchSetDomainResolver(mockService);
|
||||
|
||||
// Execute resolver
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
BatchSetDomainInput input = new BatchSetDomainInput(null, ImmutableList.of(
|
||||
new ResourceRefInput(TEST_ENTITY_URN_1, null, null),
|
||||
new ResourceRefInput(TEST_ENTITY_URN_2, null, null)));
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
|
||||
Mockito.verify(mockService, Mockito.times(0)).ingestProposal(
|
||||
Mockito.any(),
|
||||
Mockito.any(AuditStamp.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUnauthorized() throws Exception {
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
|
||||
BatchSetDomainResolver resolver = new BatchSetDomainResolver(mockService);
|
||||
|
||||
// Execute resolver
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
BatchSetDomainInput input = new BatchSetDomainInput(null, ImmutableList.of(
|
||||
new ResourceRefInput(TEST_ENTITY_URN_1, null, null),
|
||||
new ResourceRefInput(TEST_ENTITY_URN_2, null, null)));
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
QueryContext mockContext = getMockDenyContext();
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
|
||||
Mockito.verify(mockService, Mockito.times(0)).ingestProposal(
|
||||
Mockito.any(),
|
||||
Mockito.any(AuditStamp.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetEntityClientException() throws Exception {
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
|
||||
Mockito.doThrow(RuntimeException.class).when(mockService).ingestProposal(
|
||||
Mockito.any(),
|
||||
Mockito.any(AuditStamp.class));
|
||||
|
||||
BatchSetDomainResolver resolver = new BatchSetDomainResolver(mockService);
|
||||
|
||||
// Execute resolver
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
BatchSetDomainInput input = new BatchSetDomainInput(null, ImmutableList.of(
|
||||
new ResourceRefInput(TEST_ENTITY_URN_1, null, null),
|
||||
new ResourceRefInput(TEST_ENTITY_URN_2, null, null)));
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ import { getDataForEntityType } from '../shared/containers/profile/utils';
|
||||
import { useGetDomainQuery } from '../../../graphql/domain.generated';
|
||||
import { DomainEntitiesTab } from './DomainEntitiesTab';
|
||||
import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown';
|
||||
import { EntityActionItem } from '../shared/entity/EntityActions';
|
||||
// import { EntityActionItem } from '../shared/entity/EntityActions';
|
||||
|
||||
/**
|
||||
@ -66,7 +67,7 @@ export class DomainEntity implements Entity<Domain> {
|
||||
useUpdateQuery={undefined}
|
||||
getOverrideProperties={this.getOverridePropertiesFromEntity}
|
||||
headerDropdownItems={new Set([EntityMenuItems.COPY_URL, EntityMenuItems.DELETE])}
|
||||
// headerActionItems={new Set([EntityActionItem.BATCH_ADD_DOMAIN])}
|
||||
headerActionItems={new Set([EntityActionItem.BATCH_ADD_DOMAIN])}
|
||||
isNameEditable
|
||||
tabs={[
|
||||
{
|
||||
|
||||
@ -19,6 +19,7 @@ import { SelectActionGroups } from './types';
|
||||
const DEFAULT_ACTION_GROUPS = [
|
||||
SelectActionGroups.CHANGE_TAGS,
|
||||
SelectActionGroups.CHANGE_GLOSSARY_TERMS,
|
||||
SelectActionGroups.CHANGE_DOMAINS,
|
||||
SelectActionGroups.CHANGE_OWNERS,
|
||||
];
|
||||
|
||||
@ -94,6 +95,7 @@ export const SearchSelectActions = ({
|
||||
selectedEntityUrns.length === 0 ||
|
||||
!isEntityCapabilitySupported(EntityCapabilityType.DOMAINS, selectedEntityTypes)
|
||||
}
|
||||
refetch={refetch}
|
||||
/>
|
||||
)}
|
||||
{visibleActionGroups.has(SelectActionGroups.CHANGE_DEPRECATION) && (
|
||||
|
||||
@ -1,27 +1,79 @@
|
||||
import React from 'react';
|
||||
import { message, Modal } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { useBatchSetDomainMutation } from '../../../../../../../graphql/mutations.generated';
|
||||
import { SetDomainModal } from '../../../../containers/profile/sidebar/Domain/SetDomainModal';
|
||||
import ActionDropdown from './ActionDropdown';
|
||||
|
||||
type Props = {
|
||||
urns: Array<string>;
|
||||
disabled: boolean;
|
||||
refetch?: () => void;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line
|
||||
export default function DomainsDropdown({ urns, disabled = false }: Props) {
|
||||
export default function DomainsDropdown({ urns, disabled = false, refetch }: Props) {
|
||||
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
|
||||
const [batchSetDomainMutation] = useBatchSetDomainMutation();
|
||||
|
||||
const batchUnsetDomains = () => {
|
||||
batchSetDomainMutation({
|
||||
variables: {
|
||||
input: {
|
||||
resources: [...urns.map((urn) => ({ resourceUrn: urn }))],
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ errors }) => {
|
||||
if (!errors) {
|
||||
message.success({ content: 'Removed Domain!', duration: 2 });
|
||||
refetch?.();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
message.destroy();
|
||||
message.error({ content: `Failed to remove assets from Domain: \n ${e.message || ''}`, duration: 3 });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionDropdown
|
||||
name="Domains"
|
||||
actions={[
|
||||
{
|
||||
title: 'Set domain',
|
||||
onClick: () => null,
|
||||
},
|
||||
{
|
||||
title: 'Unset domain',
|
||||
onClick: () => null,
|
||||
},
|
||||
]}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<>
|
||||
<ActionDropdown
|
||||
name="Domain"
|
||||
actions={[
|
||||
{
|
||||
title: 'Set Domain',
|
||||
onClick: () => {
|
||||
setIsEditModalVisible(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Unset Domain',
|
||||
onClick: () => {
|
||||
Modal.confirm({
|
||||
title: `If you continue, Domain will be removed for the selected assets.`,
|
||||
content: `Are you sure you want to unset Domain for these assets?`,
|
||||
onOk() {
|
||||
batchUnsetDomains();
|
||||
},
|
||||
onCancel() {},
|
||||
okText: 'Yes',
|
||||
maskClosable: true,
|
||||
closable: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
]}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{isEditModalVisible && (
|
||||
<SetDomainModal
|
||||
urns={urns}
|
||||
onCloseModal={() => {
|
||||
setIsEditModalVisible(false);
|
||||
refetch?.();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,14 +4,14 @@ import styled from 'styled-components';
|
||||
|
||||
import { useGetSearchResultsLazyQuery } from '../../../../../../../graphql/search.generated';
|
||||
import { Entity, EntityType } from '../../../../../../../types.generated';
|
||||
import { useSetDomainMutation } from '../../../../../../../graphql/mutations.generated';
|
||||
import { useBatchSetDomainMutation } from '../../../../../../../graphql/mutations.generated';
|
||||
import { useEntityRegistry } from '../../../../../../useEntityRegistry';
|
||||
import { useMutationUrn } from '../../../../EntityContext';
|
||||
import { useEnterKeyListener } from '../../../../../../shared/useEnterKeyListener';
|
||||
import { useGetRecommendations } from '../../../../../../shared/recommendation';
|
||||
import { DomainLabel } from '../../../../../../shared/DomainLabel';
|
||||
|
||||
type Props = {
|
||||
urns: string[];
|
||||
onCloseModal: () => void;
|
||||
refetch?: () => Promise<any>;
|
||||
};
|
||||
@ -30,15 +30,14 @@ const StyleTag = styled(Tag)`
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const SetDomainModal = ({ onCloseModal, refetch }: Props) => {
|
||||
export const SetDomainModal = ({ urns, onCloseModal, refetch }: Props) => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const urn = useMutationUrn();
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [selectedDomain, setSelectedDomain] = useState<SelectedDomain | undefined>(undefined);
|
||||
const [domainSearch, { data: domainSearchData }] = useGetSearchResultsLazyQuery();
|
||||
const domainSearchResults =
|
||||
domainSearchData?.search?.searchResults?.map((searchResult) => searchResult.entity) || [];
|
||||
const [setDomainMutation] = useSetDomainMutation();
|
||||
const [batchSetDomainMutation] = useBatchSetDomainMutation();
|
||||
const [recommendedData] = useGetRecommendations([EntityType.Domain]);
|
||||
const inputEl = useRef(null);
|
||||
|
||||
@ -103,23 +102,25 @@ export const SetDomainModal = ({ onCloseModal, refetch }: Props) => {
|
||||
if (!selectedDomain) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await setDomainMutation({
|
||||
variables: {
|
||||
entityUrn: urn,
|
||||
domainUrn: selectedDomain.urn,
|
||||
batchSetDomainMutation({
|
||||
variables: {
|
||||
input: {
|
||||
resources: [...urns.map((urn) => ({ resourceUrn: urn }))],
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ errors }) => {
|
||||
if (!errors) {
|
||||
message.success({ content: 'Updated Domain!', duration: 2 });
|
||||
refetch?.();
|
||||
onModalClose();
|
||||
setSelectedDomain(undefined);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
message.destroy();
|
||||
message.error({ content: `Failed to add assets to Domain: \n ${e.message || ''}`, duration: 3 });
|
||||
});
|
||||
message.success({ content: 'Updated Domain!', duration: 2 });
|
||||
} catch (e: unknown) {
|
||||
message.destroy();
|
||||
if (e instanceof Error) {
|
||||
message.error({ content: `Failed to set Domain: \n ${e.message || ''}`, duration: 3 });
|
||||
}
|
||||
}
|
||||
setSelectedDomain(undefined);
|
||||
refetch?.();
|
||||
onModalClose();
|
||||
};
|
||||
|
||||
const selectValue = (selectedDomain && [selectedDomain?.displayName]) || undefined;
|
||||
|
||||
@ -2,7 +2,7 @@ import { Typography, Button, Modal, message } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
import { EMPTY_MESSAGES } from '../../../../constants';
|
||||
import { useEntityData, useRefetch } from '../../../../EntityContext';
|
||||
import { useEntityData, useMutationUrn, useRefetch } from '../../../../EntityContext';
|
||||
import { SidebarHeader } from '../SidebarHeader';
|
||||
import { SetDomainModal } from './SetDomainModal';
|
||||
import { useEntityRegistry } from '../../../../../../useEntityRegistry';
|
||||
@ -14,6 +14,7 @@ export const SidebarDomainSection = () => {
|
||||
const { entityData } = useEntityData();
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const refetch = useRefetch();
|
||||
const urn = useMutationUrn();
|
||||
const [unsetDomainMutation] = useUnsetDomainMutation();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const domain = entityData?.domain?.domain;
|
||||
@ -74,6 +75,7 @@ export const SidebarDomainSection = () => {
|
||||
</div>
|
||||
{showModal && (
|
||||
<SetDomainModal
|
||||
urns={[urn]}
|
||||
refetch={refetch}
|
||||
onCloseModal={() => {
|
||||
setShowModal(false);
|
||||
|
||||
@ -4,7 +4,7 @@ import { LinkOutlined } from '@ant-design/icons';
|
||||
import { SearchSelectModal } from '../components/styled/search/SearchSelectModal';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import { EntityCapabilityType } from '../../Entity';
|
||||
import { useBatchAddTermsMutation } from '../../../../graphql/mutations.generated';
|
||||
import { useBatchAddTermsMutation, useBatchSetDomainMutation } from '../../../../graphql/mutations.generated';
|
||||
|
||||
export enum EntityActionItem {
|
||||
/**
|
||||
@ -30,6 +30,7 @@ function EntityActions(props: Props) {
|
||||
const [isBatchAddGlossaryTermModalVisible, setIsBatchAddGlossaryTermModalVisible] = useState(false);
|
||||
const [isBatchSetDomainModalVisible, setIsBatchSetDomainModalVisible] = useState(false);
|
||||
const [batchAddTermsMutation] = useBatchAddTermsMutation();
|
||||
const [batchSetDomainMutation] = useBatchSetDomainMutation();
|
||||
|
||||
// eslint-disable-next-line
|
||||
const batchAddGlossaryTerms = (entityUrns: Array<string>) => {
|
||||
@ -60,10 +61,31 @@ function EntityActions(props: Props) {
|
||||
};
|
||||
|
||||
// eslint-disable-next-line
|
||||
const batchSetDomains = (entityUrns: Array<string>) => {
|
||||
refetchForEntity?.();
|
||||
setIsBatchSetDomainModalVisible(false);
|
||||
message.success('Successfully added assets!');
|
||||
const batchSetDomain = (entityUrns: Array<string>) => {
|
||||
batchSetDomainMutation({
|
||||
variables: {
|
||||
input: {
|
||||
domainUrn: urn,
|
||||
resources: entityUrns.map((entityUrn) => ({
|
||||
resourceUrn: entityUrn,
|
||||
})),
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ errors }) => {
|
||||
if (!errors) {
|
||||
refetchForEntity?.();
|
||||
setIsBatchSetDomainModalVisible(false);
|
||||
message.success({
|
||||
content: `Added assets to Domain!`,
|
||||
duration: 2,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
message.destroy();
|
||||
message.error({ content: `Failed to add assets to Domain: \n ${e.message || ''}`, duration: 3 });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@ -95,7 +117,7 @@ function EntityActions(props: Props) {
|
||||
<SearchSelectModal
|
||||
titleText="Add assets to Domain"
|
||||
continueText="Add"
|
||||
onContinue={batchSetDomains}
|
||||
onContinue={batchSetDomain}
|
||||
onCancel={() => setIsBatchSetDomainModalVisible(false)}
|
||||
fixedEntityTypes={Array.from(
|
||||
entityRegistry.getTypesWithSupportedCapabilities(EntityCapabilityType.DOMAINS),
|
||||
|
||||
@ -89,3 +89,7 @@ mutation addTerms($input: AddTermsInput!) {
|
||||
mutation updateName($input: UpdateNameInput!) {
|
||||
updateName(input: $input)
|
||||
}
|
||||
|
||||
mutation batchSetDomain($input: BatchSetDomainInput!) {
|
||||
batchSetDomain(input: $input)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user