mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-30 03:18:24 +00:00
feat(terms) Add ability to Add and Remove Related Terms to Glossary Terms (#5120)
This commit is contained in:
parent
c677a06fd8
commit
58dc909a80
@ -86,12 +86,14 @@ import com.linkedin.datahub.graphql.resolvers.domain.ListDomainsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.domain.SetDomainResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.domain.UnsetDomainResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.entity.EntityExistsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.glossary.AddRelatedTermsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.glossary.CreateGlossaryNodeResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.glossary.CreateGlossaryTermResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.glossary.DeleteGlossaryEntityResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.glossary.GetRootGlossaryNodesResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.glossary.GetRootGlossaryTermsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.glossary.ParentNodesResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.glossary.RemoveRelatedTermsResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.group.AddGroupMembersResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.group.CreateGroupResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.group.EntityCountsResolver;
|
||||
@ -695,6 +697,8 @@ public class GmsGraphQLEngine {
|
||||
.dataFetcher("updateParentNode", new UpdateParentNodeResolver(entityService))
|
||||
.dataFetcher("deleteGlossaryEntity", new DeleteGlossaryEntityResolver(this.entityClient, this.entityService))
|
||||
.dataFetcher("updateName", new UpdateNameResolver(entityService))
|
||||
.dataFetcher("addRelatedTerms", new AddRelatedTermsResolver(this.entityService))
|
||||
.dataFetcher("removeRelatedTerms", new RemoveRelatedTermsResolver(this.entityService))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,122 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.glossary;
|
||||
|
||||
import com.linkedin.common.GlossaryTermUrnArray;
|
||||
import com.linkedin.common.urn.GlossaryTermUrn;
|
||||
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.exception.AuthorizationException;
|
||||
import com.linkedin.datahub.graphql.generated.RelatedTermsInput;
|
||||
import com.linkedin.datahub.graphql.generated.TermRelationshipType;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.glossary.GlossaryRelatedTerms;
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
|
||||
import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.*;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class AddRelatedTermsResolver implements DataFetcher<CompletableFuture<Boolean>> {
|
||||
|
||||
private final EntityService _entityService;
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throws Exception {
|
||||
|
||||
final QueryContext context = environment.getContext();
|
||||
final RelatedTermsInput input = bindArgument(environment.getArgument("input"), RelatedTermsInput.class);
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (AuthorizationUtils.canManageGlossaries(context)) {
|
||||
try {
|
||||
final TermRelationshipType relationshipType = input.getRelationshipType();
|
||||
final Urn urn = Urn.createFromString(input.getUrn());
|
||||
final List<Urn> termUrns = input.getTermUrns().stream()
|
||||
.map(UrnUtils::getUrn)
|
||||
.collect(Collectors.toList());
|
||||
validateRelatedTermsInput(urn, termUrns);
|
||||
Urn actor = Urn.createFromString(((QueryContext) context).getActorUrn());
|
||||
|
||||
GlossaryRelatedTerms glossaryRelatedTerms = (GlossaryRelatedTerms) getAspectFromEntity(
|
||||
urn.toString(),
|
||||
Constants.GLOSSARY_RELATED_TERM_ASPECT_NAME,
|
||||
_entityService,
|
||||
null
|
||||
);
|
||||
if (glossaryRelatedTerms == null) {
|
||||
glossaryRelatedTerms = new GlossaryRelatedTerms();
|
||||
}
|
||||
|
||||
if (relationshipType == TermRelationshipType.isA) {
|
||||
if (!glossaryRelatedTerms.hasIsRelatedTerms()) {
|
||||
glossaryRelatedTerms.setIsRelatedTerms(new GlossaryTermUrnArray());
|
||||
}
|
||||
final GlossaryTermUrnArray existingTermUrns = glossaryRelatedTerms.getIsRelatedTerms();
|
||||
|
||||
return updateRelatedTerms(termUrns, existingTermUrns, urn, glossaryRelatedTerms, actor);
|
||||
} else {
|
||||
if (!glossaryRelatedTerms.hasHasRelatedTerms()) {
|
||||
glossaryRelatedTerms.setHasRelatedTerms(new GlossaryTermUrnArray());
|
||||
}
|
||||
final GlossaryTermUrnArray existingTermUrns = glossaryRelatedTerms.getHasRelatedTerms();
|
||||
|
||||
return updateRelatedTerms(termUrns, existingTermUrns, urn, glossaryRelatedTerms, actor);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(String.format("Failed to add related terms to %s", input.getUrn()), e);
|
||||
}
|
||||
}
|
||||
throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator.");
|
||||
});
|
||||
}
|
||||
|
||||
public Boolean validateRelatedTermsInput(Urn urn, List<Urn> termUrns) {
|
||||
if (!urn.getEntityType().equals(Constants.GLOSSARY_TERM_ENTITY_NAME) || !_entityService.exists(urn)) {
|
||||
throw new IllegalArgumentException(String.format("Failed to update %s. %s either does not exist or is not a glossaryTerm.", urn, urn));
|
||||
}
|
||||
|
||||
for (Urn termUrn : termUrns) {
|
||||
if (termUrn.equals(urn)) {
|
||||
throw new IllegalArgumentException(String.format("Failed to update %s. Tried to create related term with itself.", urn));
|
||||
} else if (!termUrn.getEntityType().equals(Constants.GLOSSARY_TERM_ENTITY_NAME)) {
|
||||
throw new IllegalArgumentException(String.format("Failed to update %s. %s is not a glossaryTerm.", urn, termUrn));
|
||||
} else if (!_entityService.exists(termUrn)) {
|
||||
throw new IllegalArgumentException(String.format("Failed to update %s. %s does not exist.", urn, termUrn));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Boolean updateRelatedTerms(List<Urn> termUrns, GlossaryTermUrnArray existingTermUrns, Urn urn, GlossaryRelatedTerms glossaryRelatedTerms, Urn actor) {
|
||||
List<Urn> termsToAdd = new ArrayList<>();
|
||||
for (Urn termUrn : termUrns) {
|
||||
if (existingTermUrns.stream().anyMatch(association -> association.equals(termUrn))) {
|
||||
continue;
|
||||
}
|
||||
termsToAdd.add(termUrn);
|
||||
}
|
||||
|
||||
if (termsToAdd.size() == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (Urn termUrn : termsToAdd) {
|
||||
GlossaryTermUrn newUrn = new GlossaryTermUrn(termUrn.getId());
|
||||
|
||||
existingTermUrns.add(newUrn);
|
||||
}
|
||||
persistAspect(urn, Constants.GLOSSARY_RELATED_TERM_ASPECT_NAME, glossaryRelatedTerms, actor, _entityService);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.glossary;
|
||||
|
||||
import com.linkedin.common.GlossaryTermUrnArray;
|
||||
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.exception.AuthorizationException;
|
||||
import com.linkedin.datahub.graphql.generated.RelatedTermsInput;
|
||||
import com.linkedin.datahub.graphql.generated.TermRelationshipType;
|
||||
import com.linkedin.glossary.GlossaryRelatedTerms;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
|
||||
import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.getAspectFromEntity;
|
||||
import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.persistAspect;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class RemoveRelatedTermsResolver implements DataFetcher<CompletableFuture<Boolean>> {
|
||||
|
||||
private final EntityService _entityService;
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throws Exception {
|
||||
|
||||
final QueryContext context = environment.getContext();
|
||||
final RelatedTermsInput input = bindArgument(environment.getArgument("input"), RelatedTermsInput.class);
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (AuthorizationUtils.canManageGlossaries(context)) {
|
||||
try {
|
||||
final TermRelationshipType relationshipType = input.getRelationshipType();
|
||||
final Urn urn = Urn.createFromString(input.getUrn());
|
||||
final List<Urn> termUrnsToRemove = input.getTermUrns().stream()
|
||||
.map(UrnUtils::getUrn)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!urn.getEntityType().equals(Constants.GLOSSARY_TERM_ENTITY_NAME) || !_entityService.exists(urn)) {
|
||||
throw new IllegalArgumentException(String.format("Failed to update %s. %s either does not exist or is not a glossaryTerm.", urn, urn));
|
||||
}
|
||||
|
||||
Urn actor = Urn.createFromString(((QueryContext) context).getActorUrn());
|
||||
|
||||
GlossaryRelatedTerms glossaryRelatedTerms = (GlossaryRelatedTerms) getAspectFromEntity(
|
||||
urn.toString(),
|
||||
Constants.GLOSSARY_RELATED_TERM_ASPECT_NAME,
|
||||
_entityService,
|
||||
null
|
||||
);
|
||||
if (glossaryRelatedTerms == null) {
|
||||
throw new RuntimeException(String.format("Related Terms for this Urn do not exist: %s", urn));
|
||||
}
|
||||
|
||||
if (relationshipType == TermRelationshipType.isA) {
|
||||
if (!glossaryRelatedTerms.hasIsRelatedTerms()) {
|
||||
throw new RuntimeException("Failed to remove from GlossaryRelatedTerms as they do not exist for this Glossary Term");
|
||||
}
|
||||
final GlossaryTermUrnArray existingTermUrns = glossaryRelatedTerms.getIsRelatedTerms();
|
||||
|
||||
existingTermUrns.removeIf(termUrn -> termUrnsToRemove.stream().anyMatch(termUrn::equals));
|
||||
persistAspect(urn, Constants.GLOSSARY_RELATED_TERM_ASPECT_NAME, glossaryRelatedTerms, actor, _entityService);
|
||||
return true;
|
||||
} else {
|
||||
if (!glossaryRelatedTerms.hasHasRelatedTerms()) {
|
||||
throw new RuntimeException("Failed to remove from GlossaryRelatedTerms as they do not exist for this Glossary Term");
|
||||
}
|
||||
final GlossaryTermUrnArray existingTermUrns = glossaryRelatedTerms.getHasRelatedTerms();
|
||||
|
||||
existingTermUrns.removeIf(termUrn -> termUrnsToRemove.stream().anyMatch(termUrn::equals));
|
||||
persistAspect(urn, Constants.GLOSSARY_RELATED_TERM_ASPECT_NAME, glossaryRelatedTerms, actor, _entityService);
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(String.format("Failed to removes related terms from %s", input.getUrn()), e);
|
||||
}
|
||||
}
|
||||
throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator.");
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -390,6 +390,16 @@ type Mutation {
|
||||
Updates the name of the entity.
|
||||
"""
|
||||
updateName(input: UpdateNameInput!): Boolean
|
||||
|
||||
"""
|
||||
Add multiple related Terms to a Glossary Term to establish relationships
|
||||
"""
|
||||
addRelatedTerms(input: RelatedTermsInput!): Boolean
|
||||
|
||||
"""
|
||||
Remove multiple related Terms for a Glossary Term
|
||||
"""
|
||||
removeRelatedTerms(input: RelatedTermsInput!): Boolean
|
||||
}
|
||||
|
||||
"""
|
||||
@ -6083,6 +6093,42 @@ enum SubResourceType {
|
||||
DATASET_FIELD
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
Input provided when adding Terms to an asset
|
||||
"""
|
||||
input RelatedTermsInput {
|
||||
"""
|
||||
The Glossary Term urn to add or remove this relationship to/from
|
||||
"""
|
||||
urn: String!
|
||||
|
||||
"""
|
||||
The primary key of the Glossary Term to add or remove
|
||||
"""
|
||||
termUrns: [String!]!
|
||||
|
||||
"""
|
||||
The type of relationship we're adding or removing to/from for a Glossary Term
|
||||
"""
|
||||
relationshipType: TermRelationshipType!
|
||||
}
|
||||
|
||||
"""
|
||||
A type of Metadata Entity sub resource
|
||||
"""
|
||||
enum TermRelationshipType {
|
||||
"""
|
||||
When a Term inherits from, or has an 'Is A' relationship with another Term
|
||||
"""
|
||||
isA
|
||||
|
||||
"""
|
||||
When a Term contains, or has a 'Has A' relationship with another Term
|
||||
"""
|
||||
hasA
|
||||
}
|
||||
|
||||
"""
|
||||
Input provided when updating the association between a Metadata Entity and a Tag
|
||||
"""
|
||||
|
||||
@ -0,0 +1,255 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.glossary;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.linkedin.common.AuditStamp;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.common.urn.UrnUtils;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.generated.RelatedTermsInput;
|
||||
import com.linkedin.datahub.graphql.generated.TermRelationshipType;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.mxe.MetadataChangeProposal;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import org.mockito.Mockito;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import static com.linkedin.datahub.graphql.TestUtils.*;
|
||||
import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext;
|
||||
import static org.testng.Assert.*;
|
||||
|
||||
|
||||
public class AddRelatedTermsResolverTest {
|
||||
|
||||
private static final String TEST_ENTITY_URN = "urn:li:glossaryTerm:test-id-0";
|
||||
private static final String TEST_TERM_1_URN = "urn:li:glossaryTerm:test-id-1";
|
||||
private static final String TEST_TERM_2_URN = "urn:li:glossaryTerm:test-id-2";
|
||||
private static final String DATASET_URN = "urn:li:dataset:(test,test,test)";
|
||||
|
||||
private EntityService setUpService() {
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
Mockito.when(mockService.getAspect(
|
||||
Mockito.eq(UrnUtils.getUrn(TEST_ENTITY_URN)),
|
||||
Mockito.eq(Constants.GLOSSARY_RELATED_TERM_ASPECT_NAME),
|
||||
Mockito.eq(0L)))
|
||||
.thenReturn(null);
|
||||
return mockService;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuccessIsRelatedNonExistent() throws Exception {
|
||||
EntityService mockService = setUpService();
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(true);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_TERM_1_URN))).thenReturn(true);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_TERM_2_URN))).thenReturn(true);
|
||||
|
||||
AddRelatedTermsResolver resolver = new AddRelatedTermsResolver(mockService);
|
||||
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
RelatedTermsInput input = new RelatedTermsInput(TEST_ENTITY_URN, ImmutableList.of(
|
||||
TEST_TERM_1_URN,
|
||||
TEST_TERM_2_URN
|
||||
), TermRelationshipType.isA);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
assertTrue(resolver.get(mockEnv).get());
|
||||
|
||||
Mockito.verify(mockService, Mockito.times(1)).ingestProposal(
|
||||
Mockito.any(MetadataChangeProposal.class),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
Mockito.verify(mockService, Mockito.times(1)).exists(
|
||||
Mockito.eq(Urn.createFromString(TEST_ENTITY_URN))
|
||||
);
|
||||
Mockito.verify(mockService, Mockito.times(1)).exists(
|
||||
Mockito.eq(Urn.createFromString(TEST_TERM_1_URN))
|
||||
);
|
||||
Mockito.verify(mockService, Mockito.times(1)).exists(
|
||||
Mockito.eq(Urn.createFromString(TEST_TERM_2_URN))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuccessHasRelatedNonExistent() throws Exception {
|
||||
EntityService mockService = setUpService();
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(true);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_TERM_1_URN))).thenReturn(true);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_TERM_2_URN))).thenReturn(true);
|
||||
|
||||
AddRelatedTermsResolver resolver = new AddRelatedTermsResolver(mockService);
|
||||
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
RelatedTermsInput input = new RelatedTermsInput(TEST_ENTITY_URN, ImmutableList.of(
|
||||
TEST_TERM_1_URN,
|
||||
TEST_TERM_2_URN
|
||||
), TermRelationshipType.hasA);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
assertTrue(resolver.get(mockEnv).get());
|
||||
|
||||
Mockito.verify(mockService, Mockito.times(1)).ingestProposal(
|
||||
Mockito.any(MetadataChangeProposal.class),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
Mockito.verify(mockService, Mockito.times(1)).exists(
|
||||
Mockito.eq(Urn.createFromString(TEST_ENTITY_URN))
|
||||
);
|
||||
Mockito.verify(mockService, Mockito.times(1)).exists(
|
||||
Mockito.eq(Urn.createFromString(TEST_TERM_1_URN))
|
||||
);
|
||||
Mockito.verify(mockService, Mockito.times(1)).exists(
|
||||
Mockito.eq(Urn.createFromString(TEST_TERM_2_URN))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailAddSelfAsRelatedTerm() throws Exception {
|
||||
EntityService mockService = setUpService();
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(true);
|
||||
|
||||
AddRelatedTermsResolver resolver = new AddRelatedTermsResolver(mockService);
|
||||
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
RelatedTermsInput input = new RelatedTermsInput(TEST_ENTITY_URN, ImmutableList.of(
|
||||
TEST_ENTITY_URN
|
||||
), TermRelationshipType.hasA);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertThrows(ExecutionException.class, () -> resolver.get(mockEnv).get());
|
||||
Mockito.verify(mockService, Mockito.times(0)).ingestProposal(
|
||||
Mockito.any(MetadataChangeProposal.class),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailAddNonTermAsRelatedTerm() throws Exception {
|
||||
EntityService mockService = setUpService();
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(true);
|
||||
|
||||
AddRelatedTermsResolver resolver = new AddRelatedTermsResolver(mockService);
|
||||
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
RelatedTermsInput input = new RelatedTermsInput(TEST_ENTITY_URN, ImmutableList.of(
|
||||
DATASET_URN
|
||||
), TermRelationshipType.hasA);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertThrows(ExecutionException.class, () -> resolver.get(mockEnv).get());
|
||||
Mockito.verify(mockService, Mockito.times(0)).ingestProposal(
|
||||
Mockito.any(MetadataChangeProposal.class),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailAddNonExistentTermAsRelatedTerm() throws Exception {
|
||||
EntityService mockService = setUpService();
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(true);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_TERM_1_URN))).thenReturn(false);
|
||||
|
||||
AddRelatedTermsResolver resolver = new AddRelatedTermsResolver(mockService);
|
||||
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
RelatedTermsInput input = new RelatedTermsInput(TEST_ENTITY_URN, ImmutableList.of(
|
||||
TEST_TERM_1_URN
|
||||
), TermRelationshipType.hasA);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertThrows(ExecutionException.class, () -> resolver.get(mockEnv).get());
|
||||
Mockito.verify(mockService, Mockito.times(0)).ingestProposal(
|
||||
Mockito.any(MetadataChangeProposal.class),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailAddToNonExistentUrn() throws Exception {
|
||||
EntityService mockService = setUpService();
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(false);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_TERM_1_URN))).thenReturn(true);
|
||||
|
||||
AddRelatedTermsResolver resolver = new AddRelatedTermsResolver(mockService);
|
||||
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
RelatedTermsInput input = new RelatedTermsInput(TEST_ENTITY_URN, ImmutableList.of(
|
||||
TEST_TERM_1_URN
|
||||
), TermRelationshipType.hasA);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertThrows(ExecutionException.class, () -> resolver.get(mockEnv).get());
|
||||
Mockito.verify(mockService, Mockito.times(0)).ingestProposal(
|
||||
Mockito.any(MetadataChangeProposal.class),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailAddToNonTerm() throws Exception {
|
||||
EntityService mockService = setUpService();
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(DATASET_URN))).thenReturn(true);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_TERM_1_URN))).thenReturn(true);
|
||||
|
||||
AddRelatedTermsResolver resolver = new AddRelatedTermsResolver(mockService);
|
||||
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
RelatedTermsInput input = new RelatedTermsInput(DATASET_URN, ImmutableList.of(
|
||||
TEST_TERM_1_URN
|
||||
), TermRelationshipType.hasA);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertThrows(ExecutionException.class, () -> resolver.get(mockEnv).get());
|
||||
Mockito.verify(mockService, Mockito.times(0)).ingestProposal(
|
||||
Mockito.any(MetadataChangeProposal.class),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailNoPermissions() throws Exception {
|
||||
EntityService mockService = setUpService();
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(true);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_TERM_1_URN))).thenReturn(true);
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_TERM_2_URN))).thenReturn(true);
|
||||
|
||||
AddRelatedTermsResolver resolver = new AddRelatedTermsResolver(mockService);
|
||||
|
||||
QueryContext mockContext = getMockDenyContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
RelatedTermsInput input = new RelatedTermsInput(TEST_ENTITY_URN, ImmutableList.of(
|
||||
TEST_TERM_1_URN,
|
||||
TEST_TERM_2_URN
|
||||
), TermRelationshipType.isA);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertThrows(ExecutionException.class, () -> resolver.get(mockEnv).get());
|
||||
Mockito.verify(mockService, Mockito.times(0)).ingestProposal(
|
||||
Mockito.any(MetadataChangeProposal.class),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,166 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.glossary;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.linkedin.common.AuditStamp;
|
||||
import com.linkedin.common.GlossaryTermUrnArray;
|
||||
import com.linkedin.common.urn.GlossaryTermUrn;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.common.urn.UrnUtils;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.generated.RelatedTermsInput;
|
||||
import com.linkedin.datahub.graphql.generated.TermRelationshipType;
|
||||
import com.linkedin.glossary.GlossaryRelatedTerms;
|
||||
import com.linkedin.metadata.Constants;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.mxe.MetadataChangeProposal;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import org.mockito.Mockito;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext;
|
||||
import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext;
|
||||
import static org.testng.Assert.assertThrows;
|
||||
import static org.testng.Assert.assertTrue;
|
||||
|
||||
public class RemoveRelatedTermsResolverTest {
|
||||
|
||||
private static final String TEST_ENTITY_URN = "urn:li:glossaryTerm:test-id-0";
|
||||
private static final String TEST_TERM_1_URN = "urn:li:glossaryTerm:test-id-1";
|
||||
private static final String TEST_TERM_2_URN = "urn:li:glossaryTerm:test-id-2";
|
||||
|
||||
@Test
|
||||
public void testGetSuccessIsA() throws Exception {
|
||||
GlossaryTermUrn term1Urn = GlossaryTermUrn.createFromString(TEST_TERM_1_URN);
|
||||
GlossaryTermUrn term2Urn = GlossaryTermUrn.createFromString(TEST_TERM_2_URN);
|
||||
final GlossaryRelatedTerms relatedTerms = new GlossaryRelatedTerms();
|
||||
relatedTerms.setIsRelatedTerms(new GlossaryTermUrnArray(Arrays.asList(term1Urn, term2Urn)));
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
Mockito.when(mockService.getAspect(
|
||||
Mockito.eq(UrnUtils.getUrn(TEST_ENTITY_URN)),
|
||||
Mockito.eq(Constants.GLOSSARY_RELATED_TERM_ASPECT_NAME),
|
||||
Mockito.eq(0L)))
|
||||
.thenReturn(relatedTerms);
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(true);
|
||||
|
||||
RemoveRelatedTermsResolver resolver = new RemoveRelatedTermsResolver(mockService);
|
||||
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
RelatedTermsInput input = new RelatedTermsInput(TEST_ENTITY_URN, ImmutableList.of(
|
||||
TEST_TERM_1_URN
|
||||
), TermRelationshipType.isA);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertTrue(resolver.get(mockEnv).get());
|
||||
Mockito.verify(mockService, Mockito.times(1)).ingestProposal(
|
||||
Mockito.any(MetadataChangeProposal.class),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
Mockito.verify(mockService, Mockito.times(1)).exists(
|
||||
Mockito.eq(Urn.createFromString(TEST_ENTITY_URN))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuccessHasA() throws Exception {
|
||||
GlossaryTermUrn term1Urn = GlossaryTermUrn.createFromString(TEST_TERM_1_URN);
|
||||
GlossaryTermUrn term2Urn = GlossaryTermUrn.createFromString(TEST_TERM_2_URN);
|
||||
final GlossaryRelatedTerms relatedTerms = new GlossaryRelatedTerms();
|
||||
relatedTerms.setHasRelatedTerms(new GlossaryTermUrnArray(Arrays.asList(term1Urn, term2Urn)));
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
Mockito.when(mockService.getAspect(
|
||||
Mockito.eq(UrnUtils.getUrn(TEST_ENTITY_URN)),
|
||||
Mockito.eq(Constants.GLOSSARY_RELATED_TERM_ASPECT_NAME),
|
||||
Mockito.eq(0L)))
|
||||
.thenReturn(relatedTerms);
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(true);
|
||||
|
||||
RemoveRelatedTermsResolver resolver = new RemoveRelatedTermsResolver(mockService);
|
||||
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
RelatedTermsInput input = new RelatedTermsInput(TEST_ENTITY_URN, ImmutableList.of(
|
||||
TEST_TERM_1_URN
|
||||
), TermRelationshipType.hasA);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertTrue(resolver.get(mockEnv).get());
|
||||
Mockito.verify(mockService, Mockito.times(1)).ingestProposal(
|
||||
Mockito.any(MetadataChangeProposal.class),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
Mockito.verify(mockService, Mockito.times(1)).exists(
|
||||
Mockito.eq(Urn.createFromString(TEST_ENTITY_URN))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailAspectDoesNotExist() throws Exception {
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
Mockito.when(mockService.getAspect(
|
||||
Mockito.eq(UrnUtils.getUrn(TEST_ENTITY_URN)),
|
||||
Mockito.eq(Constants.GLOSSARY_RELATED_TERM_ASPECT_NAME),
|
||||
Mockito.eq(0L)))
|
||||
.thenReturn(null);
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(true);
|
||||
|
||||
RemoveRelatedTermsResolver resolver = new RemoveRelatedTermsResolver(mockService);
|
||||
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
RelatedTermsInput input = new RelatedTermsInput(TEST_ENTITY_URN, ImmutableList.of(
|
||||
TEST_TERM_1_URN
|
||||
), TermRelationshipType.hasA);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertThrows(ExecutionException.class, () -> resolver.get(mockEnv).get());
|
||||
Mockito.verify(mockService, Mockito.times(0)).ingestProposal(
|
||||
Mockito.any(MetadataChangeProposal.class),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailNoPermissions() throws Exception {
|
||||
GlossaryTermUrn term1Urn = GlossaryTermUrn.createFromString(TEST_TERM_1_URN);
|
||||
GlossaryTermUrn term2Urn = GlossaryTermUrn.createFromString(TEST_TERM_2_URN);
|
||||
final GlossaryRelatedTerms relatedTerms = new GlossaryRelatedTerms();
|
||||
relatedTerms.setIsRelatedTerms(new GlossaryTermUrnArray(Arrays.asList(term1Urn, term2Urn)));
|
||||
EntityService mockService = Mockito.mock(EntityService.class);
|
||||
Mockito.when(mockService.getAspect(
|
||||
Mockito.eq(UrnUtils.getUrn(TEST_ENTITY_URN)),
|
||||
Mockito.eq(Constants.GLOSSARY_RELATED_TERM_ASPECT_NAME),
|
||||
Mockito.eq(0L)))
|
||||
.thenReturn(relatedTerms);
|
||||
|
||||
Mockito.when(mockService.exists(Urn.createFromString(TEST_ENTITY_URN))).thenReturn(true);
|
||||
|
||||
RemoveRelatedTermsResolver resolver = new RemoveRelatedTermsResolver(mockService);
|
||||
|
||||
QueryContext mockContext = getMockDenyContext();
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
RelatedTermsInput input = new RelatedTermsInput(TEST_ENTITY_URN, ImmutableList.of(
|
||||
TEST_TERM_1_URN
|
||||
), TermRelationshipType.isA);
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
assertThrows(ExecutionException.class, () -> resolver.get(mockEnv).get());
|
||||
Mockito.verify(mockService, Mockito.times(0)).ingestProposal(
|
||||
Mockito.any(MetadataChangeProposal.class),
|
||||
Mockito.any(AuditStamp.class)
|
||||
);
|
||||
Mockito.verify(mockService, Mockito.times(0)).exists(
|
||||
Mockito.eq(Urn.createFromString(TEST_ENTITY_URN))
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,205 @@
|
||||
import { message, Button, Modal, Select, Tag } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { useAddRelatedTermsMutation } from '../../../../graphql/glossaryTerm.generated';
|
||||
import { useGetSearchResultsLazyQuery } from '../../../../graphql/search.generated';
|
||||
import { EntityType, SearchResult, TermRelationshipType } from '../../../../types.generated';
|
||||
import GlossaryBrowser from '../../../glossary/GlossaryBrowser/GlossaryBrowser';
|
||||
import ClickOutside from '../../../shared/ClickOutside';
|
||||
import { BrowserWrapper } from '../../../shared/tags/AddTagsTermsModal';
|
||||
import TermLabel from '../../../shared/TermLabel';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import { useEntityData, useRefetch } from '../../shared/EntityContext';
|
||||
|
||||
const StyledSelect = styled(Select)`
|
||||
width: 480px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
relationshipType: TermRelationshipType;
|
||||
}
|
||||
|
||||
function AddRelatedTermsModal(props: Props) {
|
||||
const { onClose, relationshipType } = props;
|
||||
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [selectedUrns, setSelectedUrns] = useState<any[]>([]);
|
||||
const [selectedTerms, setSelectedTerms] = useState<any[]>([]);
|
||||
const [isFocusedOnInput, setIsFocusedOnInput] = useState(false);
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const { urn: entityDataUrn } = useEntityData();
|
||||
const refetch = useRefetch();
|
||||
|
||||
const [AddRelatedTerms] = useAddRelatedTermsMutation();
|
||||
|
||||
function addTerms() {
|
||||
AddRelatedTerms({
|
||||
variables: {
|
||||
input: {
|
||||
urn: entityDataUrn,
|
||||
termUrns: selectedUrns,
|
||||
relationshipType,
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((e) => {
|
||||
message.destroy();
|
||||
message.error({ content: `Failed to move: \n ${e.message || ''}`, duration: 3 });
|
||||
})
|
||||
.finally(() => {
|
||||
message.loading({ content: 'Adding...', duration: 2 });
|
||||
setTimeout(() => {
|
||||
message.success({
|
||||
content: 'Added Related Terms!',
|
||||
duration: 2,
|
||||
});
|
||||
refetch();
|
||||
}, 2000);
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
|
||||
const [termSearch, { data: termSearchData }] = useGetSearchResultsLazyQuery();
|
||||
const termSearchResults = termSearchData?.search?.searchResults || [];
|
||||
|
||||
const tagSearchOptions = termSearchResults.map((result: SearchResult) => {
|
||||
const displayName = entityRegistry.getDisplayName(result.entity.type, result.entity);
|
||||
|
||||
return (
|
||||
<Select.Option value={result.entity.urn} key={result.entity.urn} name={displayName}>
|
||||
<TermLabel name={displayName} />
|
||||
</Select.Option>
|
||||
);
|
||||
});
|
||||
|
||||
const handleSearch = (text: string) => {
|
||||
if (text.length > 0) {
|
||||
termSearch({
|
||||
variables: {
|
||||
input: {
|
||||
type: EntityType.GlossaryTerm,
|
||||
query: text,
|
||||
start: 0,
|
||||
count: 20,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// When a Tag or term search result is selected, add the urn to the Urns
|
||||
const onSelectValue = (urn: string) => {
|
||||
const newUrns = [...selectedUrns, urn];
|
||||
setSelectedUrns(newUrns);
|
||||
const selectedSearchOption = tagSearchOptions.find((option) => option.props.value === urn);
|
||||
setSelectedTerms([...selectedTerms, { urn, component: <TermLabel name={selectedSearchOption?.props.name} /> }]);
|
||||
};
|
||||
|
||||
// When a Tag or term search result is deselected, remove the urn from the Owners
|
||||
const onDeselectValue = (urn: string) => {
|
||||
const newUrns = selectedUrns.filter((u) => u !== urn);
|
||||
setSelectedUrns(newUrns);
|
||||
setInputValue('');
|
||||
setIsFocusedOnInput(true);
|
||||
setSelectedTerms(selectedTerms.filter((term) => term.urn !== urn));
|
||||
};
|
||||
|
||||
function selectTermFromBrowser(urn: string, displayName: string) {
|
||||
setIsFocusedOnInput(false);
|
||||
const newUrns = [...selectedUrns, urn];
|
||||
setSelectedUrns(newUrns);
|
||||
setSelectedTerms([...selectedTerms, { urn, component: <TermLabel name={displayName} /> }]);
|
||||
}
|
||||
|
||||
function clearInput() {
|
||||
setInputValue('');
|
||||
setTimeout(() => setIsFocusedOnInput(true), 0); // call after click outside
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
setInputValue('');
|
||||
}
|
||||
|
||||
const tagRender = (properties) => {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const { closable, onClose: close, value } = properties;
|
||||
const onPreventMouseDown = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
const selectedItem = selectedTerms.find((term) => term.urn === value).component;
|
||||
|
||||
return (
|
||||
<Tag
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={close}
|
||||
style={{
|
||||
marginRight: 3,
|
||||
display: 'flex',
|
||||
justifyContent: 'start',
|
||||
alignItems: 'center',
|
||||
whiteSpace: 'nowrap',
|
||||
opacity: 1,
|
||||
color: '#434343',
|
||||
lineHeight: '16px',
|
||||
}}
|
||||
>
|
||||
{selectedItem}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
const isShowingGlossaryBrowser = !inputValue && isFocusedOnInput;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Add Related Terms"
|
||||
visible
|
||||
onCancel={onClose}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose} type="text">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={addTerms} disabled={!selectedUrns.length}>
|
||||
Add
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<ClickOutside onClickOutside={() => setIsFocusedOnInput(false)}>
|
||||
<StyledSelect
|
||||
autoFocus
|
||||
mode="multiple"
|
||||
filterOption={false}
|
||||
placeholder="Search for Glossary Terms..."
|
||||
showSearch
|
||||
defaultActiveFirstOption={false}
|
||||
onSelect={(asset: any) => onSelectValue(asset)}
|
||||
onDeselect={(asset: any) => onDeselectValue(asset)}
|
||||
onSearch={(value: string) => {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
handleSearch(value.trim());
|
||||
// eslint-disable-next-line react/prop-types
|
||||
setInputValue(value.trim());
|
||||
}}
|
||||
tagRender={tagRender}
|
||||
value={selectedUrns}
|
||||
onClear={clearInput}
|
||||
onFocus={() => setIsFocusedOnInput(true)}
|
||||
onBlur={handleBlur}
|
||||
dropdownStyle={isShowingGlossaryBrowser || !inputValue ? { display: 'none' } : {}}
|
||||
>
|
||||
{tagSearchOptions}
|
||||
</StyledSelect>
|
||||
<BrowserWrapper isHidden={!isShowingGlossaryBrowser}>
|
||||
<GlossaryBrowser isSelecting selectTerm={selectTermFromBrowser} />
|
||||
</BrowserWrapper>
|
||||
</ClickOutside>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddRelatedTermsModal;
|
||||
@ -1,26 +1,23 @@
|
||||
import { Menu } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import styled from 'styled-components/macro';
|
||||
import { useEntityData } from '../../shared/EntityContext';
|
||||
import GlossaryRelatedTermsResult from './GlossaryRelatedTermsResult';
|
||||
|
||||
export enum RelatedTermTypes {
|
||||
hasRelatedTerms = 'Contains',
|
||||
isRelatedTerms = 'Inherits',
|
||||
}
|
||||
import GlossaryRelatedTermsResult, { RelatedTermTypes } from './GlossaryRelatedTermsResult';
|
||||
|
||||
const DetailWrapper = styled.div`
|
||||
display: inline-flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const MenuWrapper = styled.div`
|
||||
border: 2px solid #f5f5f5;
|
||||
border-right: 2px solid #f5f5f5;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
margin-left: 32px;
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export default function GlossayRelatedTerms() {
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
import { QueryResult } from '@apollo/client';
|
||||
import { Divider, List, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { GetGlossaryTermQuery, useGetGlossaryTermQuery } from '../../../../graphql/glossaryTerm.generated';
|
||||
import { EntityType, Exact } from '../../../../types.generated';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Typography } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { TermRelationshipType } from '../../../../types.generated';
|
||||
import { Message } from '../../../shared/Message';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import { PreviewType } from '../../Entity';
|
||||
import { ANTD_GRAY } from '../../shared/constants';
|
||||
import AddRelatedTermsModal from './AddRelatedTermsModal';
|
||||
import RelatedTerm from './RelatedTerm';
|
||||
|
||||
export enum RelatedTermTypes {
|
||||
hasRelatedTerms = 'Contains',
|
||||
isRelatedTerms = 'Inherits',
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
glossaryRelatedTermType: string;
|
||||
@ -14,47 +19,32 @@ export type Props = {
|
||||
};
|
||||
|
||||
const ListContainer = styled.div`
|
||||
display: default;
|
||||
flex-grow: default;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
align-items: center;
|
||||
border-bottom: solid 1px ${ANTD_GRAY[4]};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 15px 20px;
|
||||
margin-bottom: 30px;
|
||||
`;
|
||||
|
||||
const ListItem = styled.div`
|
||||
margin: 40px;
|
||||
padding-bottom: 5px;
|
||||
`;
|
||||
|
||||
const Profile = styled.div`
|
||||
marging-bottom: 20px;
|
||||
`;
|
||||
|
||||
const messageStyle = { marginTop: '10%' };
|
||||
|
||||
export default function GlossaryRelatedTermsResult({ glossaryRelatedTermType, glossaryRelatedTermResult }: Props) {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const [isShowingAddModal, setIsShowingAddModal] = useState(false);
|
||||
const glossaryRelatedTermUrns: Array<string> = [];
|
||||
glossaryRelatedTermResult.forEach((item: any) => {
|
||||
glossaryRelatedTermUrns.push(item?.entity?.urn);
|
||||
});
|
||||
const glossaryTermInfo: QueryResult<GetGlossaryTermQuery, Exact<{ urn: string }>>[] = [];
|
||||
const contentLoading = false;
|
||||
const relationshipType =
|
||||
glossaryRelatedTermType === RelatedTermTypes.hasRelatedTerms
|
||||
? TermRelationshipType.HasA
|
||||
: TermRelationshipType.IsA;
|
||||
|
||||
for (let i = 0; i < glossaryRelatedTermUrns.length; i++) {
|
||||
glossaryTermInfo.push(
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useGetGlossaryTermQuery({
|
||||
variables: {
|
||||
urn: glossaryRelatedTermUrns[i],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const contentLoading = glossaryTermInfo.some((item) => {
|
||||
return item.loading;
|
||||
});
|
||||
return (
|
||||
<>
|
||||
{contentLoading ? (
|
||||
@ -62,28 +52,21 @@ export default function GlossaryRelatedTermsResult({ glossaryRelatedTermType, gl
|
||||
) : (
|
||||
<ListContainer>
|
||||
<TitleContainer>
|
||||
<Typography.Title level={3}>{glossaryRelatedTermType}</Typography.Title>
|
||||
<Divider />
|
||||
<Typography.Title style={{ margin: '0' }} level={3}>
|
||||
{glossaryRelatedTermType}
|
||||
</Typography.Title>
|
||||
<Button type="text" onClick={() => setIsShowingAddModal(true)}>
|
||||
<PlusOutlined /> Add Terms
|
||||
</Button>
|
||||
</TitleContainer>
|
||||
<List
|
||||
dataSource={glossaryTermInfo}
|
||||
renderItem={(item) => {
|
||||
return (
|
||||
<ListItem>
|
||||
<Profile>
|
||||
{entityRegistry.renderPreview(
|
||||
EntityType.GlossaryTerm,
|
||||
PreviewType.PREVIEW,
|
||||
item?.data?.glossaryTerm,
|
||||
)}
|
||||
</Profile>
|
||||
<Divider />
|
||||
</ListItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{glossaryRelatedTermUrns.map((urn) => (
|
||||
<RelatedTerm key={urn} urn={urn} relationshipType={relationshipType} />
|
||||
))}
|
||||
</ListContainer>
|
||||
)}
|
||||
{isShowingAddModal && (
|
||||
<AddRelatedTermsModal onClose={() => setIsShowingAddModal(false)} relationshipType={relationshipType} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
import { DeleteOutlined, MoreOutlined } from '@ant-design/icons';
|
||||
import { Divider, Dropdown, Menu } from 'antd';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { useGetGlossaryTermQuery } from '../../../../graphql/glossaryTerm.generated';
|
||||
import { EntityType, TermRelationshipType } from '../../../../types.generated';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import { PreviewType } from '../../Entity';
|
||||
import useRemoveRelatedTerms from './useRemoveRelatedTerms';
|
||||
|
||||
const ListItem = styled.div`
|
||||
margin: 0 20px;
|
||||
`;
|
||||
|
||||
const Profile = styled.div`
|
||||
display: felx;
|
||||
marging-bottom: 20px;
|
||||
`;
|
||||
|
||||
const MenuIcon = styled(MoreOutlined)`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
height: 32px;
|
||||
margin-left: -10px;
|
||||
`;
|
||||
|
||||
const MenuItem = styled.div`
|
||||
font-size: 12px;
|
||||
padding: 0 4px;
|
||||
color: #262626;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
urn: string;
|
||||
relationshipType: TermRelationshipType;
|
||||
}
|
||||
|
||||
function RelatedTerm(props: Props) {
|
||||
const { urn, relationshipType } = props;
|
||||
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const { data, loading } = useGetGlossaryTermQuery({ variables: { urn } });
|
||||
let displayName = '';
|
||||
if (data) {
|
||||
displayName = entityRegistry.getDisplayName(EntityType.GlossaryTerm, data.glossaryTerm);
|
||||
}
|
||||
const { onRemove } = useRemoveRelatedTerms(urn, relationshipType, displayName);
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
return (
|
||||
<ListItem>
|
||||
<Profile>
|
||||
{entityRegistry.renderPreview(EntityType.GlossaryTerm, PreviewType.PREVIEW, data?.glossaryTerm)}
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.Item key="0">
|
||||
<MenuItem onClick={onRemove}>
|
||||
<DeleteOutlined /> Remove Term
|
||||
</MenuItem>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
trigger={['click']}
|
||||
>
|
||||
<MenuIcon />
|
||||
</Dropdown>
|
||||
</Profile>
|
||||
<Divider style={{ margin: '20px 0' }} />
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default RelatedTerm;
|
||||
@ -0,0 +1,60 @@
|
||||
import { message, Modal } from 'antd';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import { useEntityData, useRefetch } from '../../shared/EntityContext';
|
||||
import { useRemoveRelatedTermsMutation } from '../../../../graphql/glossaryTerm.generated';
|
||||
import { TermRelationshipType } from '../../../../types.generated';
|
||||
|
||||
function useRemoveRelatedTerms(termUrn: string, relationshipType: TermRelationshipType, displayName: string) {
|
||||
const { urn, entityType } = useEntityData();
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const refetch = useRefetch();
|
||||
|
||||
const [removeRelatedTerms] = useRemoveRelatedTermsMutation();
|
||||
|
||||
function handleRemoveRelatedTerms() {
|
||||
removeRelatedTerms({
|
||||
variables: {
|
||||
input: {
|
||||
urn,
|
||||
termUrns: [termUrn],
|
||||
relationshipType,
|
||||
},
|
||||
},
|
||||
})
|
||||
.catch((e) => {
|
||||
message.destroy();
|
||||
message.error({ content: `Failed to remove: \n ${e.message || ''}`, duration: 3 });
|
||||
})
|
||||
.finally(() => {
|
||||
message.loading({
|
||||
content: 'Removing...',
|
||||
duration: 2,
|
||||
});
|
||||
setTimeout(() => {
|
||||
refetch();
|
||||
message.success({
|
||||
content: `Removed Glossary Term!`,
|
||||
duration: 2,
|
||||
});
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function onRemove() {
|
||||
Modal.confirm({
|
||||
title: `Remove ${displayName}`,
|
||||
content: `Are you sure you want to remove this ${entityRegistry.getEntityName(entityType)}?`,
|
||||
onOk() {
|
||||
handleRemoveRelatedTerms();
|
||||
},
|
||||
onCancel() {},
|
||||
okText: 'Yes',
|
||||
maskClosable: true,
|
||||
closable: true,
|
||||
});
|
||||
}
|
||||
|
||||
return { onRemove };
|
||||
}
|
||||
|
||||
export default useRemoveRelatedTerms;
|
||||
@ -63,7 +63,6 @@ const ContentContainer = styled.div`
|
||||
const HeaderAndTabs = styled.div`
|
||||
flex-grow: 1;
|
||||
min-width: 640px;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const HeaderAndTabsFlex = styled.div`
|
||||
|
||||
@ -78,3 +78,11 @@ mutation createGlossaryTerm($input: CreateGlossaryEntityInput!) {
|
||||
mutation createGlossaryNode($input: CreateGlossaryEntityInput!) {
|
||||
createGlossaryNode(input: $input)
|
||||
}
|
||||
|
||||
mutation addRelatedTerms($input: RelatedTermsInput!) {
|
||||
addRelatedTerms(input: $input)
|
||||
}
|
||||
|
||||
mutation removeRelatedTerms($input: RelatedTermsInput!) {
|
||||
removeRelatedTerms(input: $input)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user