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.RemoveTermResolver;
|
||||||
import com.linkedin.datahub.graphql.resolvers.mutate.UpdateDescriptionResolver;
|
import com.linkedin.datahub.graphql.resolvers.mutate.UpdateDescriptionResolver;
|
||||||
import com.linkedin.datahub.graphql.resolvers.mutate.UpdateDisplayPropertiesResolver;
|
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.UpdateNameResolver;
|
||||||
import com.linkedin.datahub.graphql.resolvers.mutate.UpdateParentNodeResolver;
|
import com.linkedin.datahub.graphql.resolvers.mutate.UpdateParentNodeResolver;
|
||||||
import com.linkedin.datahub.graphql.resolvers.mutate.UpdateUserSettingResolver;
|
import com.linkedin.datahub.graphql.resolvers.mutate.UpdateUserSettingResolver;
|
||||||
@ -1205,6 +1206,7 @@ public class GmsGraphQLEngine {
|
|||||||
.dataFetcher(
|
.dataFetcher(
|
||||||
"batchRemoveOwners", new BatchRemoveOwnersResolver(entityService, entityClient))
|
"batchRemoveOwners", new BatchRemoveOwnersResolver(entityService, entityClient))
|
||||||
.dataFetcher("addLink", new AddLinkResolver(entityService, this.entityClient))
|
.dataFetcher("addLink", new AddLinkResolver(entityService, this.entityClient))
|
||||||
|
.dataFetcher("updateLink", new UpdateLinkResolver(entityService, this.entityClient))
|
||||||
.dataFetcher("removeLink", new RemoveLinkResolver(entityService, entityClient))
|
.dataFetcher("removeLink", new RemoveLinkResolver(entityService, entityClient))
|
||||||
.dataFetcher("addGroupMembers", new AddGroupMembersResolver(this.groupService))
|
.dataFetcher("addGroupMembers", new AddGroupMembersResolver(this.groupService))
|
||||||
.dataFetcher("removeGroupMembers", new RemoveGroupMembersResolver(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);
|
bindArgument(environment.getArgument("input"), RemoveLinkInput.class);
|
||||||
|
|
||||||
String linkUrl = input.getLinkUrl();
|
String linkUrl = input.getLinkUrl();
|
||||||
|
String label = input.getLabel();
|
||||||
Urn targetUrn = Urn.createFromString(input.getResourceUrn());
|
Urn targetUrn = Urn.createFromString(input.getResourceUrn());
|
||||||
|
|
||||||
if (!LinkUtils.isAuthorizedToUpdateLinks(context, targetUrn)
|
if (!LinkUtils.isAuthorizedToUpdateLinks(context, targetUrn)
|
||||||
@ -49,7 +50,7 @@ public class RemoveLinkResolver implements DataFetcher<CompletableFuture<Boolean
|
|||||||
|
|
||||||
Urn actor = CorpuserUrn.createFromString(context.getActorUrn());
|
Urn actor = CorpuserUrn.createFromString(context.getActorUrn());
|
||||||
LinkUtils.removeLink(
|
LinkUtils.removeLink(
|
||||||
context.getOperationContext(), linkUrl, targetUrn, actor, _entityService);
|
context.getOperationContext(), linkUrl, label, targetUrn, actor, _entityService);
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error(
|
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.EntityService;
|
||||||
import com.linkedin.metadata.entity.EntityUtils;
|
import com.linkedin.metadata.entity.EntityUtils;
|
||||||
import io.datahubproject.metadata.context.OperationContext;
|
import io.datahubproject.metadata.context.OperationContext;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@ -53,9 +55,12 @@ public class LinkUtils {
|
|||||||
entityService);
|
entityService);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void removeLink(
|
public static void updateLink(
|
||||||
@Nonnull OperationContext opContext,
|
@Nonnull OperationContext opContext,
|
||||||
String linkUrl,
|
String currentLinkUrl,
|
||||||
|
String currentLinkLabel,
|
||||||
|
String newLinkUrl,
|
||||||
|
String newLinkLabel,
|
||||||
Urn resourceUrn,
|
Urn resourceUrn,
|
||||||
Urn actor,
|
Urn actor,
|
||||||
EntityService<?> entityService) {
|
EntityService<?> entityService) {
|
||||||
@ -67,7 +72,39 @@ public class LinkUtils {
|
|||||||
Constants.INSTITUTIONAL_MEMORY_ASPECT_NAME,
|
Constants.INSTITUTIONAL_MEMORY_ASPECT_NAME,
|
||||||
entityService,
|
entityService,
|
||||||
new InstitutionalMemory());
|
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(
|
persistAspect(
|
||||||
opContext,
|
opContext,
|
||||||
resourceUrn,
|
resourceUrn,
|
||||||
@ -85,25 +122,92 @@ public class LinkUtils {
|
|||||||
|
|
||||||
InstitutionalMemoryMetadataArray linksArray = institutionalMemoryAspect.getElements();
|
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();
|
InstitutionalMemoryMetadata newLink = new InstitutionalMemoryMetadata();
|
||||||
newLink.setUrl(new Url(linkUrl));
|
newLink.setUrl(new Url(linkUrl));
|
||||||
newLink.setCreateStamp(EntityUtils.getAuditStamp(actor));
|
newLink.setCreateStamp(EntityUtils.getAuditStamp(actor));
|
||||||
newLink.setDescription(linkLabel); // We no longer support, this is really a label.
|
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);
|
linksArray.add(newLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void removeLink(InstitutionalMemory institutionalMemoryAspect, String linkUrl) {
|
private static void removeLink(
|
||||||
|
InstitutionalMemory institutionalMemoryAspect, String linkUrl, String label) {
|
||||||
if (!institutionalMemoryAspect.hasElements()) {
|
if (!institutionalMemoryAspect.hasElements()) {
|
||||||
institutionalMemoryAspect.setElements(new InstitutionalMemoryMetadataArray());
|
institutionalMemoryAspect.setElements(new InstitutionalMemoryMetadataArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
InstitutionalMemoryMetadataArray elementsArray = institutionalMemoryAspect.getElements();
|
InstitutionalMemoryMetadataArray elementsArray = institutionalMemoryAspect.getElements();
|
||||||
elementsArray.removeIf(link -> link.getUrl().toString().equals(linkUrl));
|
|
||||||
|
// 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) {
|
public static boolean isAuthorizedToUpdateLinks(@Nonnull QueryContext context, Urn resourceUrn) {
|
||||||
@ -118,28 +222,44 @@ public class LinkUtils {
|
|||||||
context, resourceUrn.getEntityType(), resourceUrn.toString(), orPrivilegeGroups);
|
context, resourceUrn.getEntityType(), resourceUrn.toString(), orPrivilegeGroups);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Boolean validateAddRemoveInput(
|
public static void validateAddRemoveInput(
|
||||||
@Nonnull OperationContext opContext,
|
@Nonnull OperationContext opContext,
|
||||||
String linkUrl,
|
String linkUrl,
|
||||||
Urn resourceUrn,
|
Urn resourceUrn,
|
||||||
EntityService<?> entityService) {
|
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 {
|
try {
|
||||||
new Url(linkUrl);
|
new Url(url);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
String.format(
|
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));
|
resourceUrn));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateResourceUrn(
|
||||||
|
@Nonnull OperationContext opContext, Urn resourceUrn, EntityService<?> entityService) {
|
||||||
if (!entityService.exists(opContext, resourceUrn, true)) {
|
if (!entityService.exists(opContext, resourceUrn, true)) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
String.format(
|
String.format(
|
||||||
"Failed to change institutional memory for resource %s. Resource does not exist.",
|
"Failed to change institutional memory for resource %s. Resource does not exist.",
|
||||||
resourceUrn));
|
resourceUrn));
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,9 @@ public class InstitutionalMemoryMetadataMapper {
|
|||||||
result.setAuthor(getAuthor(input.getCreateStamp().getActor().toString()));
|
result.setAuthor(getAuthor(input.getCreateStamp().getActor().toString()));
|
||||||
result.setActor(ResolvedActorMapper.map(input.getCreateStamp().getActor()));
|
result.setActor(ResolvedActorMapper.map(input.getCreateStamp().getActor()));
|
||||||
result.setCreated(AuditStampMapper.map(context, input.getCreateStamp()));
|
result.setCreated(AuditStampMapper.map(context, input.getCreateStamp()));
|
||||||
|
if (input.getUpdateStamp() != null) {
|
||||||
|
result.setUpdated(AuditStampMapper.map(context, input.getUpdateStamp()));
|
||||||
|
}
|
||||||
result.setAssociatedUrn(entityUrn.toString());
|
result.setAssociatedUrn(entityUrn.toString());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -604,6 +604,11 @@ type Mutation {
|
|||||||
"""
|
"""
|
||||||
addLink(input: AddLinkInput!): Boolean
|
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
|
Remove a link, or institutional memory, from a particular Entity
|
||||||
"""
|
"""
|
||||||
@ -3291,6 +3296,11 @@ type InstitutionalMemoryMetadata {
|
|||||||
"""
|
"""
|
||||||
created: AuditStamp!
|
created: AuditStamp!
|
||||||
|
|
||||||
|
"""
|
||||||
|
An AuditStamp corresponding to the updating of this resource
|
||||||
|
"""
|
||||||
|
updated: AuditStamp
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Deprecated, use label instead
|
Deprecated, use label instead
|
||||||
Description of the resource
|
Description of the resource
|
||||||
@ -9121,15 +9131,50 @@ input AddLinkInput {
|
|||||||
resourceUrn: String!
|
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 provided when removing the association between a Metadata Entity and a Link
|
||||||
"""
|
"""
|
||||||
input RemoveLinkInput {
|
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!
|
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
|
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}>
|
<Button type="text" onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>,
|
</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
|
Add
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
@ -80,7 +85,7 @@ export const AddLinkModal = ({ buttonProps, refetch }: AddLinkProps) => {
|
|||||||
>
|
>
|
||||||
<Form form={form} name="addLinkForm" onFinish={handleAdd} layout="vertical">
|
<Form form={form} name="addLinkForm" onFinish={handleAdd} layout="vertical">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
data-testid="add-link-modal-url"
|
data-testid="link-form-modal-url"
|
||||||
name="url"
|
name="url"
|
||||||
label="URL"
|
label="URL"
|
||||||
rules={[
|
rules={[
|
||||||
@ -98,7 +103,7 @@ export const AddLinkModal = ({ buttonProps, refetch }: AddLinkProps) => {
|
|||||||
<Input placeholder="https://" autoFocus />
|
<Input placeholder="https://" autoFocus />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
data-testid="add-link-modal-label"
|
data-testid="link-form-modal-label"
|
||||||
name="label"
|
name="label"
|
||||||
label="Label"
|
label="Label"
|
||||||
rules={[
|
rules={[
|
||||||
|
@ -49,7 +49,13 @@ export const LinkList = ({ refetch }: LinkListProps) => {
|
|||||||
const handleDeleteLink = async (metadata: InstitutionalMemoryMetadata) => {
|
const handleDeleteLink = async (metadata: InstitutionalMemoryMetadata) => {
|
||||||
try {
|
try {
|
||||||
await removeLinkMutation({
|
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 });
|
message.success({ content: 'Link Removed', duration: 2 });
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
@ -79,7 +85,13 @@ export const LinkList = ({ refetch }: LinkListProps) => {
|
|||||||
if (!linkDetails) return;
|
if (!linkDetails) return;
|
||||||
try {
|
try {
|
||||||
await removeLinkMutation({
|
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({
|
await addLinkMutation({
|
||||||
variables: { input: { linkUrl: formData.url, label: formData.label, resourceUrn: mutationUrn } },
|
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"
|
title="Link a Newer Version"
|
||||||
onCancel={close}
|
onCancel={close}
|
||||||
buttons={[
|
buttons={[
|
||||||
{ text: 'Cancel', variant: 'text', onClick: close },
|
{ text: 'Cancel', variant: 'text', onClick: close, key: 'Cancel' },
|
||||||
{ text: 'Create', variant: 'filled', onClick: handleLink },
|
{ text: 'Create', variant: 'filled', onClick: handleLink, key: 'Create' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
|
@ -54,8 +54,8 @@ export default function UnlinkAssetVersionModal({ urn, entityType, closeModal, v
|
|||||||
title="Are you sure?"
|
title="Are you sure?"
|
||||||
subtitle="Would you like to unlink this version?"
|
subtitle="Would you like to unlink this version?"
|
||||||
buttons={[
|
buttons={[
|
||||||
{ text: 'No', variant: 'text', onClick: closeModal },
|
{ text: 'No', variant: 'text', onClick: closeModal, key: 'no' },
|
||||||
{ text: 'Yes', variant: 'filled', onClick: handleUnlink },
|
{ text: 'Yes', variant: 'filled', onClick: handleUnlink, key: 'yes' },
|
||||||
]}
|
]}
|
||||||
onCancel={closeModal}
|
onCancel={closeModal}
|
||||||
/>
|
/>
|
||||||
|
@ -1,44 +1,47 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons';
|
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 React, { useState } from 'react';
|
||||||
|
|
||||||
import analytics, { EntityActionType, EventType } from '@app/analytics';
|
import analytics, { EntityActionType, EventType } from '@app/analytics';
|
||||||
import { useUserContext } from '@app/context/useUserContext';
|
import { useUserContext } from '@app/context/useUserContext';
|
||||||
import { useEntityData, useMutationUrn } from '@app/entity/shared/EntityContext';
|
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 { Button } from '@src/alchemy-components';
|
||||||
import { ModalButtonContainer } from '@src/app/shared/button/styledComponents';
|
|
||||||
|
|
||||||
import { useAddLinkMutation } from '@graphql/mutations.generated';
|
import { useAddLinkMutation } from '@graphql/mutations.generated';
|
||||||
|
|
||||||
type AddLinkProps = {
|
interface Props {
|
||||||
buttonProps?: Record<string, unknown>;
|
buttonProps?: Record<string, unknown>;
|
||||||
refetch?: () => Promise<any>;
|
refetch?: () => Promise<any>;
|
||||||
buttonType?: string;
|
buttonType?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
export const AddLinkModal = ({ buttonProps, refetch, buttonType }: AddLinkProps) => {
|
export const AddLinkModal = ({ buttonProps, refetch, buttonType }: Props) => {
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const mutationUrn = useMutationUrn();
|
const mutationUrn = useMutationUrn();
|
||||||
const user = useUserContext();
|
const user = useUserContext();
|
||||||
const { entityType } = useEntityData();
|
const { entityType } = useEntityData();
|
||||||
const [addLinkMutation] = useAddLinkMutation();
|
const [addLinkMutation] = useAddLinkMutation();
|
||||||
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
|
|
||||||
const showModal = () => {
|
const showModal = () => {
|
||||||
setIsModalVisible(true);
|
setIsModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
form.resetFields();
|
|
||||||
setIsModalVisible(false);
|
setIsModalVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = async (formData: any) => {
|
const handleAdd = async (formData: FormData) => {
|
||||||
if (user?.urn) {
|
if (user?.urn) {
|
||||||
try {
|
try {
|
||||||
await addLinkMutation({
|
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 });
|
message.success({ content: 'Link Added', duration: 2 });
|
||||||
analytics.event({
|
analytics.event({
|
||||||
@ -47,6 +50,7 @@ export const AddLinkModal = ({ buttonProps, refetch, buttonType }: AddLinkProps)
|
|||||||
entityUrn: mutationUrn,
|
entityUrn: mutationUrn,
|
||||||
actionType: EntityActionType.UpdateLinks,
|
actionType: EntityActionType.UpdateLinks,
|
||||||
});
|
});
|
||||||
|
handleClose();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
message.destroy();
|
message.destroy();
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
@ -54,7 +58,6 @@ export const AddLinkModal = ({ buttonProps, refetch, buttonType }: AddLinkProps)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
refetch?.();
|
refetch?.();
|
||||||
handleClose();
|
|
||||||
} else {
|
} else {
|
||||||
message.error({ content: `Error adding link: no user`, duration: 2 });
|
message.error({ content: `Error adding link: no user`, duration: 2 });
|
||||||
}
|
}
|
||||||
@ -88,56 +91,7 @@ export const AddLinkModal = ({ buttonProps, refetch, buttonType }: AddLinkProps)
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderButton(buttonType)}
|
{renderButton(buttonType)}
|
||||||
<Modal
|
<LinkFormModal variant="create" open={isModalVisible} onSubmit={handleAdd} onCancel={handleClose} />
|
||||||
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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 { DeleteOutlined, LinkOutlined } from '@ant-design/icons';
|
||||||
|
import { colors } from '@components';
|
||||||
import { Button, List, Typography, message } from 'antd';
|
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 { Link } from 'react-router-dom';
|
||||||
import styled from 'styled-components/macro';
|
import styled from 'styled-components/macro';
|
||||||
|
|
||||||
import { useEntityData } from '@app/entity/shared/EntityContext';
|
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 { ANTD_GRAY } from '@app/entityV2/shared/constants';
|
||||||
import { formatDateString } from '@app/entityV2/shared/containers/profile/utils';
|
import { formatDateString } from '@app/entityV2/shared/containers/profile/utils';
|
||||||
import { useEntityRegistry } from '@app/useEntityRegistry';
|
import { useEntityRegistry } from '@app/useEntityRegistry';
|
||||||
|
|
||||||
import { useRemoveLinkMutation } from '@graphql/mutations.generated';
|
import { useRemoveLinkMutation, useUpdateLinkMutation } from '@graphql/mutations.generated';
|
||||||
import { InstitutionalMemoryMetadata } from '@types';
|
import { InstitutionalMemoryMetadata } from '@types';
|
||||||
|
|
||||||
const LinkListItem = styled(List.Item)`
|
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`
|
const ListOffsetIcon = styled.span`
|
||||||
margin-left: -18px;
|
margin-left: -18px;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
@ -35,25 +44,82 @@ type LinkListProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const LinkList = ({ refetch }: 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 { urn: entityUrn, entityData } = useEntityData();
|
||||||
const entityRegistry = useEntityRegistry();
|
const entityRegistry = useEntityRegistry();
|
||||||
|
const [updateLinkMutation] = useUpdateLinkMutation();
|
||||||
const [removeLinkMutation] = useRemoveLinkMutation();
|
const [removeLinkMutation] = useRemoveLinkMutation();
|
||||||
const links = entityData?.institutionalMemory?.elements || [];
|
const links = entityData?.institutionalMemory?.elements || [];
|
||||||
|
|
||||||
const handleDeleteLink = async (metadata: InstitutionalMemoryMetadata) => {
|
const handleDeleteLink = useCallback(
|
||||||
try {
|
async (metadata: InstitutionalMemoryMetadata) => {
|
||||||
await removeLinkMutation({
|
try {
|
||||||
variables: { input: { linkUrl: metadata.url, resourceUrn: metadata.associatedUrn || entityUrn } },
|
await removeLinkMutation({
|
||||||
});
|
variables: {
|
||||||
message.success({ content: 'Link Removed', duration: 2 });
|
input: {
|
||||||
} catch (e: unknown) {
|
linkUrl: metadata.url,
|
||||||
message.destroy();
|
label: metadata.label,
|
||||||
if (e instanceof Error) {
|
resourceUrn: metadata.associatedUrn || entityUrn,
|
||||||
message.error({ content: `Error removing link: \n ${e.message || ''}`, duration: 2 });
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
message.success({ content: 'Link Removed', duration: 2 });
|
||||||
|
} catch (e: unknown) {
|
||||||
|
message.destroy();
|
||||||
|
if (e instanceof Error) {
|
||||||
|
message.error({ content: `Error removing link: \n ${e.message || ''}`, duration: 2 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
refetch?.();
|
||||||
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 ? (
|
return entityData ? (
|
||||||
<>
|
<>
|
||||||
@ -64,9 +130,14 @@ export const LinkList = ({ refetch }: LinkListProps) => {
|
|||||||
renderItem={(link) => (
|
renderItem={(link) => (
|
||||||
<LinkListItem
|
<LinkListItem
|
||||||
extra={
|
extra={
|
||||||
<Button onClick={() => handleDeleteLink(link)} type="text" shape="circle" danger>
|
<LinkButtonsContainer>
|
||||||
<DeleteOutlined />
|
<Button onClick={() => onEdit(link)} type="text" shape="circle">
|
||||||
</Button>
|
<Pencil size={16} color={colors.gray[500]} />
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => handleDeleteLink(link)} type="text" shape="circle" danger>
|
||||||
|
<DeleteOutlined />
|
||||||
|
</Button>
|
||||||
|
</LinkButtonsContainer>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
@ -93,6 +164,13 @@ export const LinkList = ({ refetch }: LinkListProps) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<LinkFormModal
|
||||||
|
variant="update"
|
||||||
|
open={isEditFormModalOpened}
|
||||||
|
initialValues={initialValuesOfEditForm}
|
||||||
|
onCancel={onEditFormModalClosed}
|
||||||
|
onSubmit={updateLink}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
@ -6,5 +6,6 @@ export const ActionsAndStatusSection = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
`;
|
`;
|
||||||
|
@ -34,6 +34,10 @@ mutation addLink($input: AddLinkInput!) {
|
|||||||
addLink(input: $input)
|
addLink(input: $input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutation updateLink($input: UpdateLinkInput!) {
|
||||||
|
updateLink(input: $input)
|
||||||
|
}
|
||||||
|
|
||||||
mutation removeLink($input: RemoveLinkInput!) {
|
mutation removeLink($input: RemoveLinkInput!) {
|
||||||
removeLink(input: $input)
|
removeLink(input: $input)
|
||||||
}
|
}
|
||||||
|
@ -19,4 +19,9 @@ record InstitutionalMemoryMetadata {
|
|||||||
* Audit stamp associated with creation of this record
|
* Audit stamp associated with creation of this record
|
||||||
*/
|
*/
|
||||||
createStamp: AuditStamp
|
createStamp: AuditStamp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audit stamp associated with updation of this record
|
||||||
|
*/
|
||||||
|
updateStamp: optional AuditStamp
|
||||||
}
|
}
|
@ -1152,6 +1152,11 @@
|
|||||||
"name" : "createStamp",
|
"name" : "createStamp",
|
||||||
"type" : "AuditStamp",
|
"type" : "AuditStamp",
|
||||||
"doc" : "Audit stamp associated with creation of this record"
|
"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",
|
"name" : "createStamp",
|
||||||
"type" : "AuditStamp",
|
"type" : "AuditStamp",
|
||||||
"doc" : "Audit stamp associated with creation of this record"
|
"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",
|
"name" : "createStamp",
|
||||||
"type" : "AuditStamp",
|
"type" : "AuditStamp",
|
||||||
"doc" : "Audit stamp associated with creation of this record"
|
"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",
|
"name" : "createStamp",
|
||||||
"type" : "AuditStamp",
|
"type" : "AuditStamp",
|
||||||
"doc" : "Audit stamp associated with creation of this record"
|
"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",
|
"name" : "createStamp",
|
||||||
"type" : "AuditStamp",
|
"type" : "AuditStamp",
|
||||||
"doc" : "Audit stamp associated with creation of this record"
|
"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.waitTextVisible("Test added");
|
||||||
cy.clickFirstOptionWithTestId("add-link-button");
|
cy.clickFirstOptionWithTestId("add-link-button");
|
||||||
cy.waitTextVisible("Add Link");
|
cy.waitTextVisible("Add Link");
|
||||||
cy.enterTextInTestId("add-link-modal-url", "www.test.com");
|
cy.enterTextInTestId("link-form-modal-url", "www.test.com");
|
||||||
cy.enterTextInTestId("add-link-modal-label", "Test Label");
|
cy.enterTextInTestId("link-form-modal-label", "Test Label");
|
||||||
cy.clickOptionWithTestId("add-link-modal-add-button");
|
cy.clickOptionWithTestId("link-form-modal-submit-button");
|
||||||
cy.waitTextVisible("Test Label");
|
cy.waitTextVisible("Test Label");
|
||||||
cy.goToDomainList();
|
cy.goToDomainList();
|
||||||
cy.waitTextVisible("Test added");
|
cy.waitTextVisible("Test added");
|
||||||
@ -148,9 +148,9 @@ describe("Verify nested domains test functionalities", () => {
|
|||||||
cy.waitTextVisible("Test documentation");
|
cy.waitTextVisible("Test documentation");
|
||||||
cy.clickFirstOptionWithSpecificTestId("add-link-button", 1);
|
cy.clickFirstOptionWithSpecificTestId("add-link-button", 1);
|
||||||
cy.waitTextVisible("URL");
|
cy.waitTextVisible("URL");
|
||||||
cy.enterTextInTestId("add-link-modal-url", "www.test.com");
|
cy.enterTextInTestId("link-form-modal-url", "www.test.com");
|
||||||
cy.enterTextInTestId("add-link-modal-label", "Test Label");
|
cy.enterTextInTestId("link-form-modal-label", "Test Label");
|
||||||
cy.clickOptionWithTestId("add-link-modal-add-button");
|
cy.clickOptionWithTestId("link-form-modal-submit-button");
|
||||||
cy.waitTextVisible("Test Label");
|
cy.waitTextVisible("Test Label");
|
||||||
|
|
||||||
// add owners
|
// add owners
|
||||||
@ -201,9 +201,9 @@ describe("Verify nested domains test functionalities", () => {
|
|||||||
cy.waitTextVisible("Test added");
|
cy.waitTextVisible("Test added");
|
||||||
cy.clickFirstOptionWithTestId("add-link-button");
|
cy.clickFirstOptionWithTestId("add-link-button");
|
||||||
cy.waitTextVisible("Add Link");
|
cy.waitTextVisible("Add Link");
|
||||||
cy.enterTextInTestId("add-link-modal-url", "www.test.com");
|
cy.enterTextInTestId("link-form-modal-url", "www.test.com");
|
||||||
cy.enterTextInTestId("add-link-modal-label", "Test Label");
|
cy.enterTextInTestId("link-form-modal-label", "Test Label");
|
||||||
cy.clickOptionWithTestId("add-link-modal-add-button");
|
cy.clickOptionWithTestId("link-form-modal-submit-button");
|
||||||
cy.waitTextVisible("Test Label");
|
cy.waitTextVisible("Test Label");
|
||||||
cy.goToDomainList();
|
cy.goToDomainList();
|
||||||
cy.waitTextVisible("Test added");
|
cy.waitTextVisible("Test added");
|
||||||
|
@ -152,9 +152,9 @@ describe("Verify nested domains test functionalities", () => {
|
|||||||
|
|
||||||
// Add a new link
|
// Add a new link
|
||||||
cy.clickFirstOptionWithTestId("add-link-button");
|
cy.clickFirstOptionWithTestId("add-link-button");
|
||||||
cy.enterTextInTestId("add-link-modal-url", "www.test.com");
|
cy.enterTextInTestId("link-form-modal-url", "www.test.com");
|
||||||
cy.enterTextInTestId("add-link-modal-label", "Test Label");
|
cy.enterTextInTestId("link-form-modal-label", "Test Label");
|
||||||
cy.clickOptionWithTestId("add-link-modal-add-button");
|
cy.clickOptionWithTestId("link-form-modal-submit-button");
|
||||||
|
|
||||||
// Verify link addition
|
// Verify link addition
|
||||||
cy.waitTextVisible("Test Label");
|
cy.waitTextVisible("Test Label");
|
||||||
@ -189,9 +189,9 @@ describe("Verify nested domains test functionalities", () => {
|
|||||||
|
|
||||||
// Add a new link
|
// Add a new link
|
||||||
cy.clickOptionWithTestId("add-link-button");
|
cy.clickOptionWithTestId("add-link-button");
|
||||||
cy.enterTextInTestId("add-link-modal-url", "www.test.com");
|
cy.enterTextInTestId("link-form-modal-url", "www.test.com");
|
||||||
cy.enterTextInTestId("add-link-modal-label", "Test Label");
|
cy.enterTextInTestId("link-form-modal-label", "Test Label");
|
||||||
cy.clickOptionWithTestId("add-link-modal-add-button");
|
cy.clickOptionWithTestId("link-form-modal-submit-button");
|
||||||
|
|
||||||
// Verify link addition
|
// Verify link addition
|
||||||
cy.waitTextVisible("Test Label");
|
cy.waitTextVisible("Test Label");
|
||||||
|
@ -48,14 +48,14 @@ describe("edit documentation and link to dataset", () => {
|
|||||||
.click({ force: true });
|
.click({ force: true });
|
||||||
cy.waitTextVisible("Link Removed");
|
cy.waitTextVisible("Link Removed");
|
||||||
cy.clickOptionWithTestId("add-link-button").wait(1000);
|
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.waitTextVisible("This field must be a valid url.");
|
||||||
cy.focused().clear();
|
cy.focused().clear();
|
||||||
cy.waitTextVisible("A URL is required.");
|
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.ensureTextNotPresent("This field must be a valid url.");
|
||||||
cy.enterTextInTestId("add-link-modal-label", "Sample doc");
|
cy.enterTextInTestId("link-form-modal-label", "Sample doc");
|
||||||
cy.clickOptionWithTestId("add-link-modal-add-button");
|
cy.clickOptionWithTestId("link-form-modal-submit-button");
|
||||||
cy.waitTextVisible("Link Added");
|
cy.waitTextVisible("Link Added");
|
||||||
cy.openEntityTab("Documentation");
|
cy.openEntityTab("Documentation");
|
||||||
cy.get(`[href='${correct_url}']`).should("be.visible");
|
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.visit("/domain/urn:li:domain:marketing/Entities");
|
||||||
cy.waitTextVisible("SampleCypressKafkaDataset");
|
cy.waitTextVisible("SampleCypressKafkaDataset");
|
||||||
cy.clickOptionWithTestId("add-link-button").wait(1000);
|
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.waitTextVisible("This field must be a valid url.");
|
||||||
cy.focused().clear();
|
cy.focused().clear();
|
||||||
cy.waitTextVisible("A URL is required.");
|
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.ensureTextNotPresent("This field must be a valid url.");
|
||||||
cy.enterTextInTestId("add-link-modal-label", "Sample doc");
|
cy.enterTextInTestId("link-form-modal-label", "Sample doc");
|
||||||
cy.clickOptionWithTestId("add-link-modal-add-button");
|
cy.clickOptionWithTestId("link-form-modal-submit-button");
|
||||||
cy.waitTextVisible("Link Added");
|
cy.waitTextVisible("Link Added");
|
||||||
cy.openEntityTab("Documentation");
|
cy.openEntityTab("Documentation");
|
||||||
cy.get("[data-testid='edit-documentation-button']").should("be.visible");
|
cy.get("[data-testid='edit-documentation-button']").should("be.visible");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user