feat(ui): Batch set & unset Domain for assets via the UI (#5560)

This commit is contained in:
John Joyce 2022-08-04 13:37:47 -07:00 committed by GitHub
parent 8ea5295294
commit 6c616fd417
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 641 additions and 46 deletions

View File

@ -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))

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}

View File

@ -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));

View File

@ -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
"""

View File

@ -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());
}
}

View File

@ -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={[
{

View File

@ -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) && (

View File

@ -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?.();
}}
/>
)}
</>
);
}

View File

@ -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;

View File

@ -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);

View File

@ -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),

View File

@ -89,3 +89,7 @@ mutation addTerms($input: AddTermsInput!) {
mutation updateName($input: UpdateNameInput!) {
updateName(input: $input)
}
mutation batchSetDomain($input: BatchSetDomainInput!) {
batchSetDomain(input: $input)
}