mirror of
https://github.com/datahub-project/datahub.git
synced 2025-08-28 19:16:02 +00:00
feat(links): bring edit links functionality from SaaS (#14559)
Co-authored-by: v-tarasevich-blitz-brain <v.tarasevich@blitz-brain.com>
This commit is contained in:
parent
4f98d15d9a
commit
fd053255cd
@ -168,6 +168,7 @@ import com.linkedin.datahub.graphql.resolvers.mutate.RemoveTagResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.RemoveTermResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.UpdateDescriptionResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.UpdateDisplayPropertiesResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.UpdateLinkResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.UpdateNameResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.UpdateParentNodeResolver;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.UpdateUserSettingResolver;
|
||||
@ -1205,6 +1206,7 @@ public class GmsGraphQLEngine {
|
||||
.dataFetcher(
|
||||
"batchRemoveOwners", new BatchRemoveOwnersResolver(entityService, entityClient))
|
||||
.dataFetcher("addLink", new AddLinkResolver(entityService, this.entityClient))
|
||||
.dataFetcher("updateLink", new UpdateLinkResolver(entityService, this.entityClient))
|
||||
.dataFetcher("removeLink", new RemoveLinkResolver(entityService, entityClient))
|
||||
.dataFetcher("addGroupMembers", new AddGroupMembersResolver(this.groupService))
|
||||
.dataFetcher("removeGroupMembers", new RemoveGroupMembersResolver(this.groupService))
|
||||
|
@ -32,6 +32,7 @@ public class RemoveLinkResolver implements DataFetcher<CompletableFuture<Boolean
|
||||
bindArgument(environment.getArgument("input"), RemoveLinkInput.class);
|
||||
|
||||
String linkUrl = input.getLinkUrl();
|
||||
String label = input.getLabel();
|
||||
Urn targetUrn = Urn.createFromString(input.getResourceUrn());
|
||||
|
||||
if (!LinkUtils.isAuthorizedToUpdateLinks(context, targetUrn)
|
||||
@ -49,7 +50,7 @@ public class RemoveLinkResolver implements DataFetcher<CompletableFuture<Boolean
|
||||
|
||||
Urn actor = CorpuserUrn.createFromString(context.getActorUrn());
|
||||
LinkUtils.removeLink(
|
||||
context.getOperationContext(), linkUrl, targetUrn, actor, _entityService);
|
||||
context.getOperationContext(), linkUrl, label, targetUrn, actor, _entityService);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
|
@ -0,0 +1,78 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.mutate;
|
||||
|
||||
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*;
|
||||
|
||||
import com.linkedin.common.urn.CorpuserUrn;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
|
||||
import com.linkedin.datahub.graphql.exception.AuthorizationException;
|
||||
import com.linkedin.datahub.graphql.generated.UpdateLinkInput;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.util.GlossaryUtils;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.util.LinkUtils;
|
||||
import com.linkedin.entity.client.EntityClient;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class UpdateLinkResolver implements DataFetcher<CompletableFuture<Boolean>> {
|
||||
|
||||
private final EntityService _entityService;
|
||||
private final EntityClient _entityClient;
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throws Exception {
|
||||
final QueryContext context = environment.getContext();
|
||||
final UpdateLinkInput input =
|
||||
bindArgument(environment.getArgument("input"), UpdateLinkInput.class);
|
||||
|
||||
String currentLinkUrl = input.getCurrentUrl();
|
||||
String currentLinkLabel = input.getCurrentLabel();
|
||||
String linkUrl = input.getLinkUrl();
|
||||
String linkLabel = input.getLabel();
|
||||
Urn targetUrn = Urn.createFromString(input.getResourceUrn());
|
||||
|
||||
if (!LinkUtils.isAuthorizedToUpdateLinks(context, targetUrn)
|
||||
&& !GlossaryUtils.canUpdateGlossaryEntity(targetUrn, context, _entityClient)) {
|
||||
throw new AuthorizationException(
|
||||
"Unauthorized to perform this action. Please contact your DataHub administrator.");
|
||||
}
|
||||
|
||||
return GraphQLConcurrencyUtils.supplyAsync(
|
||||
() -> {
|
||||
LinkUtils.validateUpdateInput(
|
||||
context.getOperationContext(), currentLinkUrl, linkUrl, targetUrn, _entityService);
|
||||
try {
|
||||
|
||||
log.debug("Updating Link. input: {}", input.toString());
|
||||
|
||||
Urn actor = CorpuserUrn.createFromString(context.getActorUrn());
|
||||
LinkUtils.updateLink(
|
||||
context.getOperationContext(),
|
||||
currentLinkUrl,
|
||||
currentLinkLabel,
|
||||
linkUrl,
|
||||
linkLabel,
|
||||
targetUrn,
|
||||
actor,
|
||||
_entityService);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Failed to update link to resource with input {}, {}",
|
||||
input.toString(),
|
||||
e.getMessage());
|
||||
throw new RuntimeException(
|
||||
String.format("Failed to update link to resource with input %s", input.toString()),
|
||||
e);
|
||||
}
|
||||
},
|
||||
this.getClass().getSimpleName(),
|
||||
"get");
|
||||
}
|
||||
}
|
@ -17,6 +17,8 @@ import com.linkedin.metadata.authorization.PoliciesConfig;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.metadata.entity.EntityUtils;
|
||||
import io.datahubproject.metadata.context.OperationContext;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import javax.annotation.Nonnull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@ -53,9 +55,12 @@ public class LinkUtils {
|
||||
entityService);
|
||||
}
|
||||
|
||||
public static void removeLink(
|
||||
public static void updateLink(
|
||||
@Nonnull OperationContext opContext,
|
||||
String linkUrl,
|
||||
String currentLinkUrl,
|
||||
String currentLinkLabel,
|
||||
String newLinkUrl,
|
||||
String newLinkLabel,
|
||||
Urn resourceUrn,
|
||||
Urn actor,
|
||||
EntityService<?> entityService) {
|
||||
@ -67,7 +72,39 @@ public class LinkUtils {
|
||||
Constants.INSTITUTIONAL_MEMORY_ASPECT_NAME,
|
||||
entityService,
|
||||
new InstitutionalMemory());
|
||||
removeLink(institutionalMemoryAspect, linkUrl);
|
||||
|
||||
updateLink(
|
||||
institutionalMemoryAspect,
|
||||
currentLinkUrl,
|
||||
currentLinkLabel,
|
||||
newLinkUrl,
|
||||
newLinkLabel,
|
||||
actor);
|
||||
persistAspect(
|
||||
opContext,
|
||||
resourceUrn,
|
||||
Constants.INSTITUTIONAL_MEMORY_ASPECT_NAME,
|
||||
institutionalMemoryAspect,
|
||||
actor,
|
||||
entityService);
|
||||
}
|
||||
|
||||
public static void removeLink(
|
||||
@Nonnull OperationContext opContext,
|
||||
String linkUrl,
|
||||
String label,
|
||||
Urn resourceUrn,
|
||||
Urn actor,
|
||||
EntityService<?> entityService) {
|
||||
InstitutionalMemory institutionalMemoryAspect =
|
||||
(InstitutionalMemory)
|
||||
EntityUtils.getAspectFromEntity(
|
||||
opContext,
|
||||
resourceUrn.toString(),
|
||||
Constants.INSTITUTIONAL_MEMORY_ASPECT_NAME,
|
||||
entityService,
|
||||
new InstitutionalMemory());
|
||||
removeLink(institutionalMemoryAspect, linkUrl, label);
|
||||
persistAspect(
|
||||
opContext,
|
||||
resourceUrn,
|
||||
@ -85,26 +122,93 @@ public class LinkUtils {
|
||||
|
||||
InstitutionalMemoryMetadataArray linksArray = institutionalMemoryAspect.getElements();
|
||||
|
||||
// if link exists, do not add it again
|
||||
if (linksArray.stream().anyMatch(link -> link.getUrl().toString().equals(linkUrl))) {
|
||||
return;
|
||||
}
|
||||
|
||||
InstitutionalMemoryMetadata newLink = new InstitutionalMemoryMetadata();
|
||||
newLink.setUrl(new Url(linkUrl));
|
||||
newLink.setCreateStamp(EntityUtils.getAuditStamp(actor));
|
||||
newLink.setDescription(linkLabel); // We no longer support, this is really a label.
|
||||
|
||||
// if link exists, do not add it again
|
||||
if (hasDuplicates(linksArray, newLink)) {
|
||||
return;
|
||||
}
|
||||
|
||||
linksArray.add(newLink);
|
||||
}
|
||||
|
||||
private static void removeLink(InstitutionalMemory institutionalMemoryAspect, String linkUrl) {
|
||||
private static void removeLink(
|
||||
InstitutionalMemory institutionalMemoryAspect, String linkUrl, String label) {
|
||||
if (!institutionalMemoryAspect.hasElements()) {
|
||||
institutionalMemoryAspect.setElements(new InstitutionalMemoryMetadataArray());
|
||||
}
|
||||
|
||||
InstitutionalMemoryMetadataArray elementsArray = institutionalMemoryAspect.getElements();
|
||||
|
||||
// when label is passed, it's needed to remove link with the same url and label
|
||||
if (label != null) {
|
||||
elementsArray.removeIf(getPredicate(linkUrl, label));
|
||||
} else {
|
||||
elementsArray.removeIf(link -> link.getUrl().toString().equals(linkUrl));
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateLink(
|
||||
InstitutionalMemory institutionalMemoryAspect,
|
||||
String currentLinkUrl,
|
||||
String currentLinkLabel,
|
||||
String newLinkUrl,
|
||||
String newLinkLabel,
|
||||
Urn actor) {
|
||||
if (!institutionalMemoryAspect.hasElements()) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format(
|
||||
"Failed to update the link '%s' with label '%s'. The link does not exist",
|
||||
currentLinkUrl, currentLinkLabel));
|
||||
}
|
||||
|
||||
InstitutionalMemoryMetadataArray elementsArray = institutionalMemoryAspect.getElements();
|
||||
|
||||
Optional<InstitutionalMemoryMetadata> optionalLinkToReplace =
|
||||
elementsArray.stream().filter(getPredicate(currentLinkUrl, currentLinkLabel)).findFirst();
|
||||
if (optionalLinkToReplace.isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format(
|
||||
"Failed to update the link '%s' with label '%s'. The link does not exist",
|
||||
currentLinkUrl, currentLinkLabel));
|
||||
}
|
||||
InstitutionalMemoryMetadata linkToReplace = optionalLinkToReplace.get();
|
||||
|
||||
InstitutionalMemoryMetadata updatedLink = new InstitutionalMemoryMetadata();
|
||||
updatedLink.setUrl(new Url(newLinkUrl));
|
||||
updatedLink.setDescription(newLinkLabel);
|
||||
updatedLink.setCreateStamp(linkToReplace.getCreateStamp());
|
||||
updatedLink.setUpdateStamp(EntityUtils.getAuditStamp(actor));
|
||||
|
||||
InstitutionalMemoryMetadataArray linksWithoutReplacingOne =
|
||||
new InstitutionalMemoryMetadataArray();
|
||||
linksWithoutReplacingOne.addAll(
|
||||
elementsArray.stream().filter(link -> !link.equals(linkToReplace)).toList());
|
||||
|
||||
if (hasDuplicates(linksWithoutReplacingOne, updatedLink)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("The link '%s' with label '%s' already exists", newLinkUrl, newLinkLabel));
|
||||
}
|
||||
|
||||
elementsArray.set(elementsArray.indexOf(linkToReplace), updatedLink);
|
||||
}
|
||||
|
||||
private static Predicate<InstitutionalMemoryMetadata> getPredicate(
|
||||
String linkUrl, String linkLabel) {
|
||||
return (link) ->
|
||||
link.getUrl().toString().equals(linkUrl) & link.getDescription().equals((linkLabel));
|
||||
}
|
||||
|
||||
private static boolean hasDuplicates(
|
||||
InstitutionalMemoryMetadataArray linksArray, InstitutionalMemoryMetadata linkToValidate) {
|
||||
String url = linkToValidate.getUrl().toString();
|
||||
String label = linkToValidate.getDescription();
|
||||
|
||||
return linksArray.stream().anyMatch(getPredicate(url, label));
|
||||
}
|
||||
|
||||
public static boolean isAuthorizedToUpdateLinks(@Nonnull QueryContext context, Urn resourceUrn) {
|
||||
final DisjunctivePrivilegeGroup orPrivilegeGroups =
|
||||
@ -118,28 +222,44 @@ public class LinkUtils {
|
||||
context, resourceUrn.getEntityType(), resourceUrn.toString(), orPrivilegeGroups);
|
||||
}
|
||||
|
||||
public static Boolean validateAddRemoveInput(
|
||||
public static void validateAddRemoveInput(
|
||||
@Nonnull OperationContext opContext,
|
||||
String linkUrl,
|
||||
Urn resourceUrn,
|
||||
EntityService<?> entityService) {
|
||||
validateUrl(linkUrl, resourceUrn);
|
||||
validateResourceUrn(opContext, resourceUrn, entityService);
|
||||
}
|
||||
|
||||
public static void validateUpdateInput(
|
||||
@Nonnull OperationContext opContext,
|
||||
String currentLinkUrl,
|
||||
String linkUrl,
|
||||
Urn resourceUrn,
|
||||
EntityService<?> entityService) {
|
||||
validateUrl(currentLinkUrl, resourceUrn);
|
||||
validateUrl(linkUrl, resourceUrn);
|
||||
validateResourceUrn(opContext, resourceUrn, entityService);
|
||||
}
|
||||
|
||||
private static void validateUrl(String url, Urn resourceUrn) {
|
||||
try {
|
||||
new Url(linkUrl);
|
||||
new Url(url);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format(
|
||||
"Failed to change institutional memory for resource %s. Expected a corp group urn.",
|
||||
"Failed to change institutional memory for resource %s. Expected an url.",
|
||||
resourceUrn));
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateResourceUrn(
|
||||
@Nonnull OperationContext opContext, Urn resourceUrn, EntityService<?> entityService) {
|
||||
if (!entityService.exists(opContext, resourceUrn, true)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format(
|
||||
"Failed to change institutional memory for resource %s. Resource does not exist.",
|
||||
resourceUrn));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,9 @@ public class InstitutionalMemoryMetadataMapper {
|
||||
result.setAuthor(getAuthor(input.getCreateStamp().getActor().toString()));
|
||||
result.setActor(ResolvedActorMapper.map(input.getCreateStamp().getActor()));
|
||||
result.setCreated(AuditStampMapper.map(context, input.getCreateStamp()));
|
||||
if (input.getUpdateStamp() != null) {
|
||||
result.setUpdated(AuditStampMapper.map(context, input.getUpdateStamp()));
|
||||
}
|
||||
result.setAssociatedUrn(entityUrn.toString());
|
||||
return result;
|
||||
}
|
||||
|
@ -604,6 +604,11 @@ type Mutation {
|
||||
"""
|
||||
addLink(input: AddLinkInput!): Boolean
|
||||
|
||||
"""
|
||||
Update a link, or institutional memory, from a particular Entity
|
||||
"""
|
||||
updateLink(input: UpdateLinkInput!): Boolean
|
||||
|
||||
"""
|
||||
Remove a link, or institutional memory, from a particular Entity
|
||||
"""
|
||||
@ -3291,6 +3296,11 @@ type InstitutionalMemoryMetadata {
|
||||
"""
|
||||
created: AuditStamp!
|
||||
|
||||
"""
|
||||
An AuditStamp corresponding to the updating of this resource
|
||||
"""
|
||||
updated: AuditStamp
|
||||
|
||||
"""
|
||||
Deprecated, use label instead
|
||||
Description of the resource
|
||||
@ -9121,15 +9131,50 @@ input AddLinkInput {
|
||||
resourceUrn: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Input provided when updating the association between a Metadata Entity and a Link
|
||||
"""
|
||||
input UpdateLinkInput {
|
||||
"""
|
||||
Current url of the link
|
||||
"""
|
||||
currentUrl: String!
|
||||
|
||||
"""
|
||||
Current label of the link
|
||||
"""
|
||||
currentLabel: String!
|
||||
|
||||
"""
|
||||
The new url of the link
|
||||
"""
|
||||
linkUrl: String!
|
||||
|
||||
"""
|
||||
The new label of the link
|
||||
"""
|
||||
label: String!
|
||||
|
||||
"""
|
||||
The urn of the resource or entity to attach the link to, for example a dataset urn
|
||||
"""
|
||||
resourceUrn: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Input provided when removing the association between a Metadata Entity and a Link
|
||||
"""
|
||||
input RemoveLinkInput {
|
||||
"""
|
||||
The url of the link to add or remove, which uniquely identifies the Link
|
||||
The url of the link to remove, which uniquely identifies the Link
|
||||
"""
|
||||
linkUrl: String!
|
||||
|
||||
"""
|
||||
The label of the link to remove, which uniquely identifies the Link
|
||||
"""
|
||||
label: String
|
||||
|
||||
"""
|
||||
The urn of the resource or entity to attach the link to, for example a dataset urn
|
||||
"""
|
||||
|
@ -0,0 +1,201 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.link;
|
||||
|
||||
import static com.linkedin.datahub.graphql.TestUtils.*;
|
||||
import static org.testng.Assert.assertThrows;
|
||||
|
||||
import com.datahub.authentication.Authentication;
|
||||
import com.linkedin.common.InstitutionalMemory;
|
||||
import com.linkedin.common.InstitutionalMemoryMetadata;
|
||||
import com.linkedin.common.InstitutionalMemoryMetadataArray;
|
||||
import com.linkedin.common.url.Url;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.exception.AuthorizationException;
|
||||
import com.linkedin.datahub.graphql.generated.AddLinkInput;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.AddLinkResolver;
|
||||
import com.linkedin.entity.client.EntityClient;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import org.mockito.Mockito;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
public class AddLinkResolverTest {
|
||||
private static final String ASSET_URN = "urn:li:dataset:(test1,test2,test3)";
|
||||
private static final String TEST_URL = "https://www.github.com";
|
||||
private static final String TEST_LABEL = "Test Label";
|
||||
private static final AddLinkInput TEST_INPUT = new AddLinkInput(TEST_URL, TEST_LABEL, ASSET_URN);
|
||||
|
||||
private void setupTest(DataFetchingEnvironment mockEnv) {
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
Mockito.when(mockContext.getAuthentication()).thenReturn(Mockito.mock(Authentication.class));
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuccessNoPreviousAspect() throws Exception {
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(true, null);
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
|
||||
InstitutionalMemory expectedAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadataArray elements = new InstitutionalMemoryMetadataArray();
|
||||
InstitutionalMemoryMetadata link = new InstitutionalMemoryMetadata();
|
||||
link.setUrl(new Url(TEST_URL));
|
||||
link.setDescription(TEST_LABEL);
|
||||
elements.add(link);
|
||||
expectedAspect.setElements(elements);
|
||||
|
||||
setupTest(mockEnv);
|
||||
|
||||
AddLinkResolver resolver = new AddLinkResolver(mockService, mockClient);
|
||||
resolver.get(mockEnv).get();
|
||||
|
||||
LinkTestUtils.verifyIngestInstitutionalMemory(mockService, 1, expectedAspect);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuccessWithPreviousAspect() throws Exception {
|
||||
InstitutionalMemory originalAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadataArray elements = new InstitutionalMemoryMetadataArray();
|
||||
InstitutionalMemoryMetadata link =
|
||||
LinkTestUtils.createLink("https://www.google.com", "Original Label");
|
||||
elements.add(link);
|
||||
originalAspect.setElements(elements);
|
||||
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(true, originalAspect);
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
|
||||
InstitutionalMemory expectedAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadataArray newElements = new InstitutionalMemoryMetadataArray();
|
||||
InstitutionalMemoryMetadata newLink = LinkTestUtils.createLink(TEST_URL, TEST_LABEL);
|
||||
// make sure to include existing link
|
||||
newElements.add(link);
|
||||
newElements.add(newLink);
|
||||
expectedAspect.setElements(newElements);
|
||||
|
||||
setupTest(mockEnv);
|
||||
|
||||
AddLinkResolver resolver = new AddLinkResolver(mockService, mockClient);
|
||||
resolver.get(mockEnv).get();
|
||||
|
||||
LinkTestUtils.verifyIngestInstitutionalMemory(mockService, 1, expectedAspect);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailureNoEntity() throws Exception {
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(false, null);
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
|
||||
setupTest(mockEnv);
|
||||
|
||||
AddLinkResolver resolver = new AddLinkResolver(mockService, mockClient);
|
||||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
|
||||
|
||||
LinkTestUtils.verifyIngestInstitutionalMemory(mockService, 0, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailureNoPermission() throws Exception {
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(true, null);
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
|
||||
QueryContext mockContext = getMockDenyContext();
|
||||
Mockito.when(mockContext.getAuthentication()).thenReturn(Mockito.mock(Authentication.class));
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(TEST_INPUT);
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
AddLinkResolver resolver = new AddLinkResolver(mockService, mockClient);
|
||||
assertThrows(AuthorizationException.class, () -> resolver.get(mockEnv).join());
|
||||
|
||||
LinkTestUtils.verifyIngestInstitutionalMemory(mockService, 0, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldNotAddLinkWithTheSameUrlAndLabel() throws Exception {
|
||||
InstitutionalMemory originalAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadataArray elements = new InstitutionalMemoryMetadataArray();
|
||||
InstitutionalMemoryMetadata link = LinkTestUtils.createLink(TEST_URL, TEST_LABEL);
|
||||
elements.add(link);
|
||||
originalAspect.setElements(elements);
|
||||
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(true, originalAspect);
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
|
||||
InstitutionalMemory expectedAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadataArray newElements = new InstitutionalMemoryMetadataArray();
|
||||
InstitutionalMemoryMetadata newLink = LinkTestUtils.createLink(TEST_URL, TEST_LABEL);
|
||||
// should include only already existing link
|
||||
newElements.add(link);
|
||||
expectedAspect.setElements(newElements);
|
||||
|
||||
setupTest(mockEnv);
|
||||
|
||||
AddLinkResolver resolver = new AddLinkResolver(mockService, mockClient);
|
||||
resolver.get(mockEnv).get();
|
||||
|
||||
LinkTestUtils.verifyIngestInstitutionalMemory(mockService, 1, expectedAspect);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuccessWhenLinkWithTheSameUrlAndDifferentLabelAdded() throws Exception {
|
||||
InstitutionalMemory originalAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadataArray elements = new InstitutionalMemoryMetadataArray();
|
||||
InstitutionalMemoryMetadata link = LinkTestUtils.createLink(TEST_URL, "Another label");
|
||||
|
||||
elements.add(link);
|
||||
originalAspect.setElements(elements);
|
||||
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(true, originalAspect);
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
|
||||
InstitutionalMemory expectedAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadataArray newElements = new InstitutionalMemoryMetadataArray();
|
||||
InstitutionalMemoryMetadata newLink = LinkTestUtils.createLink(TEST_URL, TEST_LABEL);
|
||||
// make sure to include existing link
|
||||
newElements.add(link);
|
||||
newElements.add(newLink);
|
||||
expectedAspect.setElements(newElements);
|
||||
|
||||
setupTest(mockEnv);
|
||||
|
||||
AddLinkResolver resolver = new AddLinkResolver(mockService, mockClient);
|
||||
resolver.get(mockEnv).get();
|
||||
|
||||
LinkTestUtils.verifyIngestInstitutionalMemory(mockService, 1, expectedAspect);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuccessWhenLinkWithDifferentUrlAndTheSameLabelAdded() throws Exception {
|
||||
InstitutionalMemory originalAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadataArray elements = new InstitutionalMemoryMetadataArray();
|
||||
InstitutionalMemoryMetadata link =
|
||||
LinkTestUtils.createLink("https://another-url.com", TEST_LABEL);
|
||||
elements.add(link);
|
||||
originalAspect.setElements(elements);
|
||||
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(true, originalAspect);
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
|
||||
InstitutionalMemory expectedAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadataArray newElements = new InstitutionalMemoryMetadataArray();
|
||||
InstitutionalMemoryMetadata newLink = LinkTestUtils.createLink(TEST_URL, TEST_LABEL);
|
||||
// make sure to include existing link
|
||||
newElements.add(link);
|
||||
newElements.add(newLink);
|
||||
expectedAspect.setElements(newElements);
|
||||
|
||||
setupTest(mockEnv);
|
||||
|
||||
AddLinkResolver resolver = new AddLinkResolver(mockService, mockClient);
|
||||
resolver.get(mockEnv).get();
|
||||
|
||||
LinkTestUtils.verifyIngestInstitutionalMemory(mockService, 1, expectedAspect);
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.link;
|
||||
|
||||
import static com.linkedin.datahub.graphql.TestUtils.*;
|
||||
import static com.linkedin.metadata.Constants.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
|
||||
import com.datahub.authentication.Authentication;
|
||||
import com.linkedin.common.AuditStamp;
|
||||
import com.linkedin.common.InstitutionalMemory;
|
||||
import com.linkedin.common.InstitutionalMemoryMetadata;
|
||||
import com.linkedin.common.url.Url;
|
||||
import com.linkedin.common.urn.Urn;
|
||||
import com.linkedin.data.template.RecordTemplate;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.entity.client.EntityClient;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import com.linkedin.metadata.entity.ebean.batch.ChangeItemImpl;
|
||||
import com.linkedin.metadata.search.SearchEntityArray;
|
||||
import com.linkedin.metadata.search.SearchResult;
|
||||
import com.linkedin.metadata.utils.GenericRecordUtils;
|
||||
import com.linkedin.mxe.MetadataChangeProposal;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import io.datahubproject.metadata.context.OperationContext;
|
||||
import java.time.Clock;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mockito;
|
||||
import org.testng.Assert;
|
||||
|
||||
public class LinkTestUtils {
|
||||
|
||||
public static InstitutionalMemoryMetadata createLink(String url, String label) throws Exception {
|
||||
InstitutionalMemoryMetadata link = new InstitutionalMemoryMetadata();
|
||||
|
||||
link.setUrl(new Url(url));
|
||||
link.setDescription(label);
|
||||
|
||||
AuditStamp createStamp = new AuditStamp();
|
||||
createStamp.setActor(new Urn("urn:corpuser:test"));
|
||||
createStamp.setTime(Clock.systemUTC().millis());
|
||||
link.setCreateStamp(createStamp);
|
||||
|
||||
return link;
|
||||
}
|
||||
|
||||
public static DataFetchingEnvironment initMockEnv() {
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
QueryContext mockContext = getMockAllowContext();
|
||||
Mockito.when(mockContext.getAuthentication()).thenReturn(Mockito.mock(Authentication.class));
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
return mockEnv;
|
||||
}
|
||||
|
||||
public static EntityService<ChangeItemImpl> initMockEntityService(
|
||||
@Nonnull Boolean entityExists, @Nullable RecordTemplate currentAspect) {
|
||||
EntityService<ChangeItemImpl> mockService = Mockito.mock(EntityService.class);
|
||||
|
||||
if (entityExists) {
|
||||
Mockito.when(
|
||||
mockService.exists(any(OperationContext.class), any(Urn.class), Mockito.eq(true)))
|
||||
.thenReturn(true);
|
||||
} else {
|
||||
Mockito.when(
|
||||
mockService.exists(any(OperationContext.class), any(Urn.class), Mockito.eq(true)))
|
||||
.thenReturn(false);
|
||||
}
|
||||
|
||||
Mockito.when(
|
||||
mockService.getAspect(
|
||||
any(OperationContext.class),
|
||||
any(Urn.class),
|
||||
Mockito.eq(INSTITUTIONAL_MEMORY_ASPECT_NAME),
|
||||
any(long.class)))
|
||||
.thenReturn(currentAspect);
|
||||
|
||||
return mockService;
|
||||
}
|
||||
|
||||
public static EntityClient initMockClient() throws Exception {
|
||||
EntityClient mockClient = Mockito.mock(EntityClient.class);
|
||||
|
||||
Mockito.when(
|
||||
mockClient.filter(
|
||||
any(),
|
||||
Mockito.eq(GLOSSARY_TERM_ENTITY_NAME),
|
||||
Mockito.any(),
|
||||
Mockito.eq(null),
|
||||
Mockito.eq(0),
|
||||
Mockito.eq(1000)))
|
||||
.thenReturn(new SearchResult().setEntities(new SearchEntityArray()));
|
||||
Mockito.when(
|
||||
mockClient.batchGetV2(
|
||||
any(),
|
||||
Mockito.eq(GLOSSARY_TERM_ENTITY_NAME),
|
||||
Mockito.any(),
|
||||
Mockito.eq(Collections.singleton(GLOSSARY_TERM_INFO_ASPECT_NAME))))
|
||||
.thenReturn(new HashMap<>());
|
||||
|
||||
return mockClient;
|
||||
}
|
||||
|
||||
static void verifyIngestInstitutionalMemory(
|
||||
EntityService<?> mockService, int numberOfInvocations, InstitutionalMemory expectedAspect) {
|
||||
ArgumentCaptor<MetadataChangeProposal> proposalCaptor =
|
||||
ArgumentCaptor.forClass(MetadataChangeProposal.class);
|
||||
|
||||
Mockito.verify(mockService, Mockito.times(numberOfInvocations))
|
||||
.ingestProposal(any(), proposalCaptor.capture(), any(AuditStamp.class), Mockito.eq(false));
|
||||
|
||||
if (numberOfInvocations > 0) {
|
||||
// check has time
|
||||
Assert.assertTrue(proposalCaptor.getValue().getSystemMetadata().getLastObserved() > 0L);
|
||||
|
||||
InstitutionalMemory actualAspect =
|
||||
GenericRecordUtils.deserializeAspect(
|
||||
proposalCaptor.getValue().getAspect().getValue(),
|
||||
proposalCaptor.getValue().getAspect().getContentType(),
|
||||
InstitutionalMemory.class);
|
||||
|
||||
Assert.assertEquals(actualAspect.getElements().size(), expectedAspect.getElements().size());
|
||||
|
||||
actualAspect
|
||||
.getElements()
|
||||
.forEach(
|
||||
element -> {
|
||||
int index = actualAspect.getElements().indexOf(element);
|
||||
InstitutionalMemoryMetadata expectedElement =
|
||||
expectedAspect.getElements().get(index);
|
||||
Assert.assertEquals(element.getUrl(), expectedElement.getUrl());
|
||||
Assert.assertEquals(element.getDescription(), expectedElement.getDescription());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.link;
|
||||
|
||||
import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext;
|
||||
import static org.testng.Assert.assertThrows;
|
||||
|
||||
import com.datahub.authentication.Authentication;
|
||||
import com.linkedin.common.*;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.exception.AuthorizationException;
|
||||
import com.linkedin.datahub.graphql.generated.RemoveLinkInput;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.RemoveLinkResolver;
|
||||
import com.linkedin.entity.client.EntityClient;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import org.mockito.Mockito;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
public class RemoveLinkResolverTest {
|
||||
private static final String ASSET_URN = "urn:li:dataset:(test1,test2,test3)";
|
||||
|
||||
private static DataFetchingEnvironment initMockEnv(RemoveLinkInput input) {
|
||||
DataFetchingEnvironment mockEnv = LinkTestUtils.initMockEnv();
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
return mockEnv;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuccessWhenRemovingExistingLinkByUrlAndLabel() throws Exception {
|
||||
InstitutionalMemory originalAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadata originalLink =
|
||||
LinkTestUtils.createLink("https://original-url.com", "Original label");
|
||||
InstitutionalMemoryMetadata originalLinkWithAnotherLabel =
|
||||
LinkTestUtils.createLink("https://original-url.com", "Another label");
|
||||
InstitutionalMemoryMetadataArray elements =
|
||||
new InstitutionalMemoryMetadataArray(originalLink, originalLinkWithAnotherLabel);
|
||||
originalAspect.setElements(elements);
|
||||
|
||||
InstitutionalMemory expectedAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadataArray expectedElements =
|
||||
new InstitutionalMemoryMetadataArray(originalLinkWithAnotherLabel);
|
||||
expectedAspect.setElements(expectedElements);
|
||||
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(true, originalAspect);
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
DataFetchingEnvironment mockEnv =
|
||||
initMockEnv(new RemoveLinkInput("https://original-url.com", "Original label", ASSET_URN));
|
||||
RemoveLinkResolver resolver = new RemoveLinkResolver(mockService, mockClient);
|
||||
resolver.get(mockEnv).get();
|
||||
|
||||
LinkTestUtils.verifyIngestInstitutionalMemory(mockService, 1, expectedAspect);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuccessWhenRemovingExistingLinkByUrl() throws Exception {
|
||||
InstitutionalMemory originalAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadata originalLink =
|
||||
LinkTestUtils.createLink("https://original-url.com", "Original label");
|
||||
InstitutionalMemoryMetadataArray elements = new InstitutionalMemoryMetadataArray(originalLink);
|
||||
originalAspect.setElements(elements);
|
||||
|
||||
InstitutionalMemory expectedAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadataArray newElements = new InstitutionalMemoryMetadataArray();
|
||||
expectedAspect.setElements(newElements);
|
||||
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(true, originalAspect);
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
DataFetchingEnvironment mockEnv =
|
||||
initMockEnv(new RemoveLinkInput("https://original-url.com", null, ASSET_URN));
|
||||
RemoveLinkResolver resolver = new RemoveLinkResolver(mockService, mockClient);
|
||||
resolver.get(mockEnv).get();
|
||||
|
||||
LinkTestUtils.verifyIngestInstitutionalMemory(mockService, 1, expectedAspect);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuccessWhenRemovingNotExistingLink() throws Exception {
|
||||
InstitutionalMemory originalAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadataArray elements = new InstitutionalMemoryMetadataArray();
|
||||
originalAspect.setElements(elements);
|
||||
|
||||
InstitutionalMemory expectedAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadataArray newElements = new InstitutionalMemoryMetadataArray();
|
||||
expectedAspect.setElements(newElements);
|
||||
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(true, originalAspect);
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
DataFetchingEnvironment mockEnv =
|
||||
initMockEnv(new RemoveLinkInput("https://original-url.com", "Original label", ASSET_URN));
|
||||
RemoveLinkResolver resolver = new RemoveLinkResolver(mockService, mockClient);
|
||||
resolver.get(mockEnv).get();
|
||||
|
||||
LinkTestUtils.verifyIngestInstitutionalMemory(mockService, 1, expectedAspect);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailureNoEntity() throws Exception {
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(false, null);
|
||||
|
||||
DataFetchingEnvironment mockEnv =
|
||||
initMockEnv(new RemoveLinkInput("https://original-url.com", "Original label", ASSET_URN));
|
||||
RemoveLinkResolver resolver = new RemoveLinkResolver(mockService, mockClient);
|
||||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
|
||||
|
||||
LinkTestUtils.verifyIngestInstitutionalMemory(mockService, 0, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailureNoPermission() throws Exception {
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(true, null);
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
|
||||
QueryContext mockContext = getMockDenyContext();
|
||||
Mockito.when(mockContext.getAuthentication()).thenReturn(Mockito.mock(Authentication.class));
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input")))
|
||||
.thenReturn(new RemoveLinkInput("https://original-url.com", "Original label", ASSET_URN));
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
RemoveLinkResolver resolver = new RemoveLinkResolver(mockService, mockClient);
|
||||
assertThrows(AuthorizationException.class, () -> resolver.get(mockEnv).join());
|
||||
|
||||
LinkTestUtils.verifyIngestInstitutionalMemory(mockService, 0, null);
|
||||
}
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
package com.linkedin.datahub.graphql.resolvers.link;
|
||||
|
||||
import static com.linkedin.datahub.graphql.TestUtils.getMockDenyContext;
|
||||
import static org.testng.Assert.assertThrows;
|
||||
|
||||
import com.datahub.authentication.Authentication;
|
||||
import com.linkedin.common.*;
|
||||
import com.linkedin.datahub.graphql.QueryContext;
|
||||
import com.linkedin.datahub.graphql.exception.AuthorizationException;
|
||||
import com.linkedin.datahub.graphql.generated.UpdateLinkInput;
|
||||
import com.linkedin.datahub.graphql.resolvers.mutate.UpdateLinkResolver;
|
||||
import com.linkedin.entity.client.EntityClient;
|
||||
import com.linkedin.metadata.entity.EntityService;
|
||||
import graphql.schema.DataFetchingEnvironment;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import org.mockito.Mockito;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
public class UpdateLinkResolverTest {
|
||||
private static final String ASSET_URN = "urn:li:dataset:(test1,test2,test3)";
|
||||
|
||||
private static DataFetchingEnvironment initMockEnv(UpdateLinkInput input) {
|
||||
DataFetchingEnvironment mockEnv = LinkTestUtils.initMockEnv();
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input"))).thenReturn(input);
|
||||
return mockEnv;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuccessWhenUpdatingExistingLink() throws Exception {
|
||||
InstitutionalMemory originalAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadata originalLink =
|
||||
LinkTestUtils.createLink("https://original-url.com", "Original label");
|
||||
InstitutionalMemoryMetadataArray elements = new InstitutionalMemoryMetadataArray(originalLink);
|
||||
originalAspect.setElements(elements);
|
||||
|
||||
InstitutionalMemory expectedAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadata updatedLink =
|
||||
LinkTestUtils.createLink("https://updated-url.com", "Updated label");
|
||||
InstitutionalMemoryMetadataArray newElements =
|
||||
new InstitutionalMemoryMetadataArray(updatedLink);
|
||||
expectedAspect.setElements(newElements);
|
||||
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(true, originalAspect);
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
DataFetchingEnvironment mockEnv =
|
||||
initMockEnv(
|
||||
new UpdateLinkInput(
|
||||
"https://original-url.com",
|
||||
"Original label",
|
||||
"https://updated-url.com",
|
||||
"Updated label",
|
||||
ASSET_URN));
|
||||
UpdateLinkResolver resolver = new UpdateLinkResolver(mockService, mockClient);
|
||||
resolver.get(mockEnv).get();
|
||||
|
||||
LinkTestUtils.verifyIngestInstitutionalMemory(mockService, 1, expectedAspect);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailedWhenUpdatingNonExistingLink() throws Exception {
|
||||
InstitutionalMemory originalAspect = new InstitutionalMemory();
|
||||
originalAspect.setElements(new InstitutionalMemoryMetadataArray());
|
||||
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(true, originalAspect);
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
DataFetchingEnvironment mockEnv =
|
||||
initMockEnv(
|
||||
new UpdateLinkInput(
|
||||
"https://original-url.com",
|
||||
"Original label",
|
||||
"https://updated-url.com",
|
||||
"Updated label",
|
||||
ASSET_URN));
|
||||
UpdateLinkResolver resolver = new UpdateLinkResolver(mockService, mockClient);
|
||||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailedWhenUpdatedLinkIsNotUnique() throws Exception {
|
||||
InstitutionalMemory originalAspect = new InstitutionalMemory();
|
||||
InstitutionalMemoryMetadata originalLink =
|
||||
LinkTestUtils.createLink("https://original-url.com", "Original label");
|
||||
InstitutionalMemoryMetadata duplicatedLink =
|
||||
LinkTestUtils.createLink("https://duplicated-url.com", "Duplicated label");
|
||||
InstitutionalMemoryMetadataArray elements =
|
||||
new InstitutionalMemoryMetadataArray(originalLink, duplicatedLink);
|
||||
originalAspect.setElements(elements);
|
||||
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(true, originalAspect);
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
DataFetchingEnvironment mockEnv =
|
||||
initMockEnv(
|
||||
new UpdateLinkInput(
|
||||
"https://original-url.com",
|
||||
"Original label",
|
||||
"https://duplicated-url.com",
|
||||
"Duplicated label",
|
||||
ASSET_URN));
|
||||
UpdateLinkResolver resolver = new UpdateLinkResolver(mockService, mockClient);
|
||||
|
||||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailureNoEntity() throws Exception {
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(false, null);
|
||||
|
||||
DataFetchingEnvironment mockEnv =
|
||||
initMockEnv(
|
||||
new UpdateLinkInput(
|
||||
"https://original-url.com",
|
||||
"Original label",
|
||||
"https://duplicated-url.com",
|
||||
"Duplicated label",
|
||||
ASSET_URN));
|
||||
UpdateLinkResolver resolver = new UpdateLinkResolver(mockService, mockClient);
|
||||
assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join());
|
||||
|
||||
LinkTestUtils.verifyIngestInstitutionalMemory(mockService, 0, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFailureNoPermission() throws Exception {
|
||||
EntityClient mockClient = LinkTestUtils.initMockClient();
|
||||
EntityService<?> mockService = LinkTestUtils.initMockEntityService(true, null);
|
||||
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
|
||||
|
||||
QueryContext mockContext = getMockDenyContext();
|
||||
Mockito.when(mockContext.getAuthentication()).thenReturn(Mockito.mock(Authentication.class));
|
||||
Mockito.when(mockEnv.getArgument(Mockito.eq("input")))
|
||||
.thenReturn(
|
||||
new UpdateLinkInput(
|
||||
"https://original-url.com",
|
||||
"Original label",
|
||||
"https://duplicated-url.com",
|
||||
"Duplicated label",
|
||||
ASSET_URN));
|
||||
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
|
||||
|
||||
UpdateLinkResolver resolver = new UpdateLinkResolver(mockService, mockClient);
|
||||
assertThrows(AuthorizationException.class, () -> resolver.get(mockEnv).join());
|
||||
|
||||
LinkTestUtils.verifyIngestInstitutionalMemory(mockService, 0, null);
|
||||
}
|
||||
}
|
@ -72,7 +72,12 @@ export const AddLinkModal = ({ buttonProps, refetch }: AddLinkProps) => {
|
||||
<Button type="text" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>,
|
||||
<Button data-testid="add-link-modal-add-button" form="addLinkForm" key="submit" htmlType="submit">
|
||||
<Button
|
||||
data-testid="link-form-modal-submit-button"
|
||||
form="addLinkForm"
|
||||
key="submit"
|
||||
htmlType="submit"
|
||||
>
|
||||
Add
|
||||
</Button>,
|
||||
]}
|
||||
@ -80,7 +85,7 @@ export const AddLinkModal = ({ buttonProps, refetch }: AddLinkProps) => {
|
||||
>
|
||||
<Form form={form} name="addLinkForm" onFinish={handleAdd} layout="vertical">
|
||||
<Form.Item
|
||||
data-testid="add-link-modal-url"
|
||||
data-testid="link-form-modal-url"
|
||||
name="url"
|
||||
label="URL"
|
||||
rules={[
|
||||
@ -98,7 +103,7 @@ export const AddLinkModal = ({ buttonProps, refetch }: AddLinkProps) => {
|
||||
<Input placeholder="https://" autoFocus />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
data-testid="add-link-modal-label"
|
||||
data-testid="link-form-modal-label"
|
||||
name="label"
|
||||
label="Label"
|
||||
rules={[
|
||||
|
@ -49,7 +49,13 @@ export const LinkList = ({ refetch }: LinkListProps) => {
|
||||
const handleDeleteLink = async (metadata: InstitutionalMemoryMetadata) => {
|
||||
try {
|
||||
await removeLinkMutation({
|
||||
variables: { input: { linkUrl: metadata.url, resourceUrn: metadata.associatedUrn || entityUrn } },
|
||||
variables: {
|
||||
input: {
|
||||
linkUrl: metadata.url,
|
||||
label: metadata.label,
|
||||
resourceUrn: metadata.associatedUrn || entityUrn,
|
||||
},
|
||||
},
|
||||
});
|
||||
message.success({ content: 'Link Removed', duration: 2 });
|
||||
} catch (e: unknown) {
|
||||
@ -79,7 +85,13 @@ export const LinkList = ({ refetch }: LinkListProps) => {
|
||||
if (!linkDetails) return;
|
||||
try {
|
||||
await removeLinkMutation({
|
||||
variables: { input: { linkUrl: linkDetails.url, resourceUrn: linkDetails.associatedUrn || entityUrn } },
|
||||
variables: {
|
||||
input: {
|
||||
linkUrl: linkDetails.url,
|
||||
label: linkDetails.label,
|
||||
resourceUrn: linkDetails.associatedUrn || entityUrn,
|
||||
},
|
||||
},
|
||||
});
|
||||
await addLinkMutation({
|
||||
variables: { input: { linkUrl: formData.url, label: formData.label, resourceUrn: mutationUrn } },
|
||||
|
@ -104,8 +104,8 @@ export default function LinkAssetVersionModal({ urn, entityType, closeModal, ref
|
||||
title="Link a Newer Version"
|
||||
onCancel={close}
|
||||
buttons={[
|
||||
{ text: 'Cancel', variant: 'text', onClick: close },
|
||||
{ text: 'Create', variant: 'filled', onClick: handleLink },
|
||||
{ text: 'Cancel', variant: 'text', onClick: close, key: 'Cancel' },
|
||||
{ text: 'Create', variant: 'filled', onClick: handleLink, key: 'Create' },
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
|
@ -54,8 +54,8 @@ export default function UnlinkAssetVersionModal({ urn, entityType, closeModal, v
|
||||
title="Are you sure?"
|
||||
subtitle="Would you like to unlink this version?"
|
||||
buttons={[
|
||||
{ text: 'No', variant: 'text', onClick: closeModal },
|
||||
{ text: 'Yes', variant: 'filled', onClick: handleUnlink },
|
||||
{ text: 'No', variant: 'text', onClick: closeModal, key: 'no' },
|
||||
{ text: 'Yes', variant: 'filled', onClick: handleUnlink, key: 'yes' },
|
||||
]}
|
||||
onCancel={closeModal}
|
||||
/>
|
||||
|
@ -1,44 +1,47 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button as AntButton, Form, Input, Modal, message } from 'antd';
|
||||
import { Button as AntButton, message } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import analytics, { EntityActionType, EventType } from '@app/analytics';
|
||||
import { useUserContext } from '@app/context/useUserContext';
|
||||
import { useEntityData, useMutationUrn } from '@app/entity/shared/EntityContext';
|
||||
import { FormData, LinkFormModal } from '@app/entityV2/shared/components/styled/LinkFormModal';
|
||||
import { Button } from '@src/alchemy-components';
|
||||
import { ModalButtonContainer } from '@src/app/shared/button/styledComponents';
|
||||
|
||||
import { useAddLinkMutation } from '@graphql/mutations.generated';
|
||||
|
||||
type AddLinkProps = {
|
||||
interface Props {
|
||||
buttonProps?: Record<string, unknown>;
|
||||
refetch?: () => Promise<any>;
|
||||
buttonType?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const AddLinkModal = ({ buttonProps, refetch, buttonType }: AddLinkProps) => {
|
||||
export const AddLinkModal = ({ buttonProps, refetch, buttonType }: Props) => {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const mutationUrn = useMutationUrn();
|
||||
const user = useUserContext();
|
||||
const { entityType } = useEntityData();
|
||||
const [addLinkMutation] = useAddLinkMutation();
|
||||
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const showModal = () => {
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
form.resetFields();
|
||||
setIsModalVisible(false);
|
||||
};
|
||||
|
||||
const handleAdd = async (formData: any) => {
|
||||
const handleAdd = async (formData: FormData) => {
|
||||
if (user?.urn) {
|
||||
try {
|
||||
await addLinkMutation({
|
||||
variables: { input: { linkUrl: formData.url, label: formData.label, resourceUrn: mutationUrn } },
|
||||
variables: {
|
||||
input: {
|
||||
linkUrl: formData.url,
|
||||
label: formData.label,
|
||||
resourceUrn: mutationUrn,
|
||||
},
|
||||
},
|
||||
});
|
||||
message.success({ content: 'Link Added', duration: 2 });
|
||||
analytics.event({
|
||||
@ -47,6 +50,7 @@ export const AddLinkModal = ({ buttonProps, refetch, buttonType }: AddLinkProps)
|
||||
entityUrn: mutationUrn,
|
||||
actionType: EntityActionType.UpdateLinks,
|
||||
});
|
||||
handleClose();
|
||||
} catch (e: unknown) {
|
||||
message.destroy();
|
||||
if (e instanceof Error) {
|
||||
@ -54,7 +58,6 @@ export const AddLinkModal = ({ buttonProps, refetch, buttonType }: AddLinkProps)
|
||||
}
|
||||
}
|
||||
refetch?.();
|
||||
handleClose();
|
||||
} else {
|
||||
message.error({ content: `Error adding link: no user`, duration: 2 });
|
||||
}
|
||||
@ -88,56 +91,7 @@ export const AddLinkModal = ({ buttonProps, refetch, buttonType }: AddLinkProps)
|
||||
return (
|
||||
<>
|
||||
{renderButton(buttonType)}
|
||||
<Modal
|
||||
title="Add Link"
|
||||
visible={isModalVisible}
|
||||
destroyOnClose
|
||||
onCancel={handleClose}
|
||||
footer={[
|
||||
<ModalButtonContainer>
|
||||
<Button variant="text" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button data-testid="add-link-modal-add-button" form="addLinkForm" key="submit">
|
||||
Add
|
||||
</Button>
|
||||
</ModalButtonContainer>,
|
||||
]}
|
||||
>
|
||||
<Form form={form} name="addLinkForm" onFinish={handleAdd} layout="vertical">
|
||||
<Form.Item
|
||||
data-testid="add-link-modal-url"
|
||||
name="url"
|
||||
label="URL"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'A URL is required.',
|
||||
},
|
||||
{
|
||||
type: 'url',
|
||||
warningOnly: true,
|
||||
message: 'This field must be a valid url.',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="https://" autoFocus />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
data-testid="add-link-modal-label"
|
||||
name="label"
|
||||
label="Label"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'A label is required.',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="A short label for this link" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<LinkFormModal variant="create" open={isModalVisible} onSubmit={handleAdd} onCancel={handleClose} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,80 @@
|
||||
import { Form } from 'antd';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { Input, Modal } from '@src/alchemy-components';
|
||||
|
||||
export interface FormData {
|
||||
url: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
initialValues?: Partial<FormData>;
|
||||
variant: 'create' | 'update';
|
||||
onSubmit: (formData: FormData) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const LinkFormModal = ({ open, initialValues, variant, onSubmit, onCancel }: Props) => {
|
||||
const [form] = Form.useForm<FormData>();
|
||||
|
||||
const onCancelHandler = useCallback(() => {
|
||||
onCancel();
|
||||
}, [onCancel]);
|
||||
|
||||
// Reset form state to initial values when the form opened/closed
|
||||
useEffect(() => {
|
||||
form.resetFields();
|
||||
}, [open, form]);
|
||||
|
||||
const title = useMemo(() => (variant === 'create' ? 'Add Link' : 'Update Link'), [variant]);
|
||||
const submitButtonText = useMemo(() => (variant === 'create' ? 'Create' : 'Update'), [variant]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
open={open}
|
||||
destroyOnClose
|
||||
onCancel={onCancelHandler}
|
||||
buttons={[
|
||||
{ text: 'Cancel', variant: 'outline', color: 'gray', onClick: onCancelHandler },
|
||||
{ text: submitButtonText, onClick: () => form.submit() },
|
||||
]}
|
||||
>
|
||||
<Form form={form} name="linkForm" onFinish={onSubmit} layout="vertical">
|
||||
<Form.Item
|
||||
data-testid="link-form-modal-url"
|
||||
name="url"
|
||||
initialValue={initialValues?.url}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'A URL is required.',
|
||||
},
|
||||
{
|
||||
type: 'url',
|
||||
message: 'This field must be a valid url.',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input label="URL" placeholder="https://" autoFocus />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
data-testid="link-form-modal-label"
|
||||
name="label"
|
||||
initialValue={initialValues?.label}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'A label is required.',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input label="Label" placeholder="A short label for this link" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -1,15 +1,18 @@
|
||||
import { DeleteOutlined, LinkOutlined } from '@ant-design/icons';
|
||||
import { colors } from '@components';
|
||||
import { Button, List, Typography, message } from 'antd';
|
||||
import React from 'react';
|
||||
import { Pencil } from 'phosphor-react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
import { useEntityData } from '@app/entity/shared/EntityContext';
|
||||
import { FormData, LinkFormModal } from '@app/entityV2/shared/components/styled/LinkFormModal';
|
||||
import { ANTD_GRAY } from '@app/entityV2/shared/constants';
|
||||
import { formatDateString } from '@app/entityV2/shared/containers/profile/utils';
|
||||
import { useEntityRegistry } from '@app/useEntityRegistry';
|
||||
|
||||
import { useRemoveLinkMutation } from '@graphql/mutations.generated';
|
||||
import { useRemoveLinkMutation, useUpdateLinkMutation } from '@graphql/mutations.generated';
|
||||
import { InstitutionalMemoryMetadata } from '@types';
|
||||
|
||||
const LinkListItem = styled(List.Item)`
|
||||
@ -25,6 +28,12 @@ const LinkListItem = styled(List.Item)`
|
||||
}
|
||||
`;
|
||||
|
||||
const LinkButtonsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const ListOffsetIcon = styled.span`
|
||||
margin-left: -18px;
|
||||
margin-right: 6px;
|
||||
@ -35,15 +44,26 @@ type LinkListProps = {
|
||||
};
|
||||
|
||||
export const LinkList = ({ refetch }: LinkListProps) => {
|
||||
const [isEditFormModalOpened, setIsEditFormModalOpened] = useState<boolean>(false);
|
||||
const [editingMetadata, setEditingMetadata] = useState<InstitutionalMemoryMetadata>();
|
||||
const [initialValuesOfEditForm, setInitialValuesOfEditForm] = useState<FormData>();
|
||||
const { urn: entityUrn, entityData } = useEntityData();
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const [updateLinkMutation] = useUpdateLinkMutation();
|
||||
const [removeLinkMutation] = useRemoveLinkMutation();
|
||||
const links = entityData?.institutionalMemory?.elements || [];
|
||||
|
||||
const handleDeleteLink = async (metadata: InstitutionalMemoryMetadata) => {
|
||||
const handleDeleteLink = useCallback(
|
||||
async (metadata: InstitutionalMemoryMetadata) => {
|
||||
try {
|
||||
await removeLinkMutation({
|
||||
variables: { input: { linkUrl: metadata.url, resourceUrn: metadata.associatedUrn || entityUrn } },
|
||||
variables: {
|
||||
input: {
|
||||
linkUrl: metadata.url,
|
||||
label: metadata.label,
|
||||
resourceUrn: metadata.associatedUrn || entityUrn,
|
||||
},
|
||||
},
|
||||
});
|
||||
message.success({ content: 'Link Removed', duration: 2 });
|
||||
} catch (e: unknown) {
|
||||
@ -53,7 +73,53 @@ export const LinkList = ({ refetch }: LinkListProps) => {
|
||||
}
|
||||
}
|
||||
refetch?.();
|
||||
};
|
||||
},
|
||||
[refetch, removeLinkMutation, entityUrn],
|
||||
);
|
||||
|
||||
const updateLink = useCallback(
|
||||
async (formData: FormData) => {
|
||||
if (!editingMetadata) return;
|
||||
|
||||
try {
|
||||
await updateLinkMutation({
|
||||
variables: {
|
||||
input: {
|
||||
currentLabel: editingMetadata.label || editingMetadata.description,
|
||||
currentUrl: editingMetadata.url,
|
||||
resourceUrn: editingMetadata.associatedUrn || entityUrn,
|
||||
label: formData.label,
|
||||
linkUrl: formData.url,
|
||||
},
|
||||
},
|
||||
});
|
||||
message.success({ content: 'Link Updated', duration: 2 });
|
||||
setIsEditFormModalOpened(false);
|
||||
} catch (e: unknown) {
|
||||
message.destroy();
|
||||
if (e instanceof Error) {
|
||||
message.error({ content: `Error updating link: \n ${e.message || ''}`, duration: 2 });
|
||||
}
|
||||
}
|
||||
refetch?.();
|
||||
},
|
||||
[updateLinkMutation, entityUrn, editingMetadata, refetch],
|
||||
);
|
||||
|
||||
const onEdit = useCallback((metadata: InstitutionalMemoryMetadata) => {
|
||||
setEditingMetadata(metadata);
|
||||
setInitialValuesOfEditForm({
|
||||
label: metadata.label || metadata.description,
|
||||
url: metadata.url,
|
||||
});
|
||||
setIsEditFormModalOpened(true);
|
||||
}, []);
|
||||
|
||||
const onEditFormModalClosed = useCallback(() => {
|
||||
setEditingMetadata(undefined);
|
||||
setIsEditFormModalOpened(false);
|
||||
setInitialValuesOfEditForm(undefined);
|
||||
}, []);
|
||||
|
||||
return entityData ? (
|
||||
<>
|
||||
@ -64,9 +130,14 @@ export const LinkList = ({ refetch }: LinkListProps) => {
|
||||
renderItem={(link) => (
|
||||
<LinkListItem
|
||||
extra={
|
||||
<LinkButtonsContainer>
|
||||
<Button onClick={() => onEdit(link)} type="text" shape="circle">
|
||||
<Pencil size={16} color={colors.gray[500]} />
|
||||
</Button>
|
||||
<Button onClick={() => handleDeleteLink(link)} type="text" shape="circle" danger>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
</LinkButtonsContainer>
|
||||
}
|
||||
>
|
||||
<List.Item.Meta
|
||||
@ -93,6 +164,13 @@ export const LinkList = ({ refetch }: LinkListProps) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<LinkFormModal
|
||||
variant="update"
|
||||
open={isEditFormModalOpened}
|
||||
initialValues={initialValuesOfEditForm}
|
||||
onCancel={onEditFormModalClosed}
|
||||
onSubmit={updateLink}
|
||||
/>
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
@ -6,5 +6,6 @@ export const ActionsAndStatusSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 5px;
|
||||
`;
|
||||
|
@ -34,6 +34,10 @@ mutation addLink($input: AddLinkInput!) {
|
||||
addLink(input: $input)
|
||||
}
|
||||
|
||||
mutation updateLink($input: UpdateLinkInput!) {
|
||||
updateLink(input: $input)
|
||||
}
|
||||
|
||||
mutation removeLink($input: RemoveLinkInput!) {
|
||||
removeLink(input: $input)
|
||||
}
|
||||
|
@ -19,4 +19,9 @@ record InstitutionalMemoryMetadata {
|
||||
* Audit stamp associated with creation of this record
|
||||
*/
|
||||
createStamp: AuditStamp
|
||||
|
||||
/**
|
||||
* Audit stamp associated with updation of this record
|
||||
*/
|
||||
updateStamp: optional AuditStamp
|
||||
}
|
@ -1152,6 +1152,11 @@
|
||||
"name" : "createStamp",
|
||||
"type" : "AuditStamp",
|
||||
"doc" : "Audit stamp associated with creation of this record"
|
||||
}, {
|
||||
"name" : "updateStamp",
|
||||
"type" : "AuditStamp",
|
||||
"doc" : "Audit stamp associated with updation of this record",
|
||||
"optional" : true
|
||||
} ]
|
||||
}
|
||||
},
|
||||
|
@ -1135,6 +1135,11 @@
|
||||
"name" : "createStamp",
|
||||
"type" : "AuditStamp",
|
||||
"doc" : "Audit stamp associated with creation of this record"
|
||||
}, {
|
||||
"name" : "updateStamp",
|
||||
"type" : "AuditStamp",
|
||||
"doc" : "Audit stamp associated with updation of this record",
|
||||
"optional" : true
|
||||
} ]
|
||||
}
|
||||
},
|
||||
|
@ -871,6 +871,11 @@
|
||||
"name" : "createStamp",
|
||||
"type" : "AuditStamp",
|
||||
"doc" : "Audit stamp associated with creation of this record"
|
||||
}, {
|
||||
"name" : "updateStamp",
|
||||
"type" : "AuditStamp",
|
||||
"doc" : "Audit stamp associated with updation of this record",
|
||||
"optional" : true
|
||||
} ]
|
||||
}
|
||||
},
|
||||
|
@ -871,6 +871,11 @@
|
||||
"name" : "createStamp",
|
||||
"type" : "AuditStamp",
|
||||
"doc" : "Audit stamp associated with creation of this record"
|
||||
}, {
|
||||
"name" : "updateStamp",
|
||||
"type" : "AuditStamp",
|
||||
"doc" : "Audit stamp associated with updation of this record",
|
||||
"optional" : true
|
||||
} ]
|
||||
}
|
||||
},
|
||||
|
@ -1135,6 +1135,11 @@
|
||||
"name" : "createStamp",
|
||||
"type" : "AuditStamp",
|
||||
"doc" : "Audit stamp associated with creation of this record"
|
||||
}, {
|
||||
"name" : "updateStamp",
|
||||
"type" : "AuditStamp",
|
||||
"doc" : "Audit stamp associated with updation of this record",
|
||||
"optional" : true
|
||||
} ]
|
||||
}
|
||||
},
|
||||
|
@ -126,9 +126,9 @@ describe("Verify nested domains test functionalities", () => {
|
||||
cy.waitTextVisible("Test added");
|
||||
cy.clickFirstOptionWithTestId("add-link-button");
|
||||
cy.waitTextVisible("Add Link");
|
||||
cy.enterTextInTestId("add-link-modal-url", "www.test.com");
|
||||
cy.enterTextInTestId("add-link-modal-label", "Test Label");
|
||||
cy.clickOptionWithTestId("add-link-modal-add-button");
|
||||
cy.enterTextInTestId("link-form-modal-url", "www.test.com");
|
||||
cy.enterTextInTestId("link-form-modal-label", "Test Label");
|
||||
cy.clickOptionWithTestId("link-form-modal-submit-button");
|
||||
cy.waitTextVisible("Test Label");
|
||||
cy.goToDomainList();
|
||||
cy.waitTextVisible("Test added");
|
||||
@ -148,9 +148,9 @@ describe("Verify nested domains test functionalities", () => {
|
||||
cy.waitTextVisible("Test documentation");
|
||||
cy.clickFirstOptionWithSpecificTestId("add-link-button", 1);
|
||||
cy.waitTextVisible("URL");
|
||||
cy.enterTextInTestId("add-link-modal-url", "www.test.com");
|
||||
cy.enterTextInTestId("add-link-modal-label", "Test Label");
|
||||
cy.clickOptionWithTestId("add-link-modal-add-button");
|
||||
cy.enterTextInTestId("link-form-modal-url", "www.test.com");
|
||||
cy.enterTextInTestId("link-form-modal-label", "Test Label");
|
||||
cy.clickOptionWithTestId("link-form-modal-submit-button");
|
||||
cy.waitTextVisible("Test Label");
|
||||
|
||||
// add owners
|
||||
@ -201,9 +201,9 @@ describe("Verify nested domains test functionalities", () => {
|
||||
cy.waitTextVisible("Test added");
|
||||
cy.clickFirstOptionWithTestId("add-link-button");
|
||||
cy.waitTextVisible("Add Link");
|
||||
cy.enterTextInTestId("add-link-modal-url", "www.test.com");
|
||||
cy.enterTextInTestId("add-link-modal-label", "Test Label");
|
||||
cy.clickOptionWithTestId("add-link-modal-add-button");
|
||||
cy.enterTextInTestId("link-form-modal-url", "www.test.com");
|
||||
cy.enterTextInTestId("link-form-modal-label", "Test Label");
|
||||
cy.clickOptionWithTestId("link-form-modal-submit-button");
|
||||
cy.waitTextVisible("Test Label");
|
||||
cy.goToDomainList();
|
||||
cy.waitTextVisible("Test added");
|
||||
|
@ -152,9 +152,9 @@ describe("Verify nested domains test functionalities", () => {
|
||||
|
||||
// Add a new link
|
||||
cy.clickFirstOptionWithTestId("add-link-button");
|
||||
cy.enterTextInTestId("add-link-modal-url", "www.test.com");
|
||||
cy.enterTextInTestId("add-link-modal-label", "Test Label");
|
||||
cy.clickOptionWithTestId("add-link-modal-add-button");
|
||||
cy.enterTextInTestId("link-form-modal-url", "www.test.com");
|
||||
cy.enterTextInTestId("link-form-modal-label", "Test Label");
|
||||
cy.clickOptionWithTestId("link-form-modal-submit-button");
|
||||
|
||||
// Verify link addition
|
||||
cy.waitTextVisible("Test Label");
|
||||
@ -189,9 +189,9 @@ describe("Verify nested domains test functionalities", () => {
|
||||
|
||||
// Add a new link
|
||||
cy.clickOptionWithTestId("add-link-button");
|
||||
cy.enterTextInTestId("add-link-modal-url", "www.test.com");
|
||||
cy.enterTextInTestId("add-link-modal-label", "Test Label");
|
||||
cy.clickOptionWithTestId("add-link-modal-add-button");
|
||||
cy.enterTextInTestId("link-form-modal-url", "www.test.com");
|
||||
cy.enterTextInTestId("link-form-modal-label", "Test Label");
|
||||
cy.clickOptionWithTestId("link-form-modal-submit-button");
|
||||
|
||||
// Verify link addition
|
||||
cy.waitTextVisible("Test Label");
|
||||
|
@ -48,14 +48,14 @@ describe("edit documentation and link to dataset", () => {
|
||||
.click({ force: true });
|
||||
cy.waitTextVisible("Link Removed");
|
||||
cy.clickOptionWithTestId("add-link-button").wait(1000);
|
||||
cy.enterTextInTestId("add-link-modal-url", wrong_url);
|
||||
cy.enterTextInTestId("link-form-modal-url", wrong_url);
|
||||
cy.waitTextVisible("This field must be a valid url.");
|
||||
cy.focused().clear();
|
||||
cy.waitTextVisible("A URL is required.");
|
||||
cy.enterTextInTestId("add-link-modal-url", correct_url);
|
||||
cy.enterTextInTestId("link-form-modal-url", correct_url);
|
||||
cy.ensureTextNotPresent("This field must be a valid url.");
|
||||
cy.enterTextInTestId("add-link-modal-label", "Sample doc");
|
||||
cy.clickOptionWithTestId("add-link-modal-add-button");
|
||||
cy.enterTextInTestId("link-form-modal-label", "Sample doc");
|
||||
cy.clickOptionWithTestId("link-form-modal-submit-button");
|
||||
cy.waitTextVisible("Link Added");
|
||||
cy.openEntityTab("Documentation");
|
||||
cy.get(`[href='${correct_url}']`).should("be.visible");
|
||||
@ -66,14 +66,14 @@ describe("edit documentation and link to dataset", () => {
|
||||
cy.visit("/domain/urn:li:domain:marketing/Entities");
|
||||
cy.waitTextVisible("SampleCypressKafkaDataset");
|
||||
cy.clickOptionWithTestId("add-link-button").wait(1000);
|
||||
cy.enterTextInTestId("add-link-modal-url", wrong_url);
|
||||
cy.enterTextInTestId("link-form-modal-url", wrong_url);
|
||||
cy.waitTextVisible("This field must be a valid url.");
|
||||
cy.focused().clear();
|
||||
cy.waitTextVisible("A URL is required.");
|
||||
cy.enterTextInTestId("add-link-modal-url", correct_url);
|
||||
cy.enterTextInTestId("link-form-modal-url", correct_url);
|
||||
cy.ensureTextNotPresent("This field must be a valid url.");
|
||||
cy.enterTextInTestId("add-link-modal-label", "Sample doc");
|
||||
cy.clickOptionWithTestId("add-link-modal-add-button");
|
||||
cy.enterTextInTestId("link-form-modal-label", "Sample doc");
|
||||
cy.clickOptionWithTestId("link-form-modal-submit-button");
|
||||
cy.waitTextVisible("Link Added");
|
||||
cy.openEntityTab("Documentation");
|
||||
cy.get("[data-testid='edit-documentation-button']").should("be.visible");
|
||||
|
Loading…
x
Reference in New Issue
Block a user