From ba33b29e41c7a145efc4c276ed8fe8199f0a8f2c Mon Sep 17 00:00:00 2001 From: amentis Date: Thu, 21 Mar 2024 09:46:18 +0200 Subject: [PATCH] fix lock and add entity locks admin page --- .../java/eu/eudat/audit/AuditableAction.java | 2 + .../eudat/commons/enums/LockTargetType.java | 4 +- .../lock/LockConfiguration.java | 20 ++ .../configurations/lock/LockProperties.java | 17 ++ .../main/java/eu/eudat/model/LockStatus.java | 24 +++ .../eu/eudat/model/persist/LockPersist.java | 8 + .../main/java/eu/eudat/query/LockQuery.java | 32 ++++ .../eu/eudat/query/lookup/LockLookup.java | 22 +++ .../eu/eudat/service/lock/LockService.java | 8 +- .../eudat/service/lock/LockServiceImpl.java | 119 ++++++------ .../eu/eudat/controllers/LockController.java | 40 +++- .../src/main/resources/config/application.yml | 4 +- .../web/src/main/resources/config/lock.yml | 2 + dmp-frontend/src/app/app-routing.module.ts | 12 ++ .../app/core/common/enum/lock-target-type.ts | 4 +- .../app/core/common/enum/permission.enum.ts | 1 + .../src/app/core/model/lock/lock.model.ts | 5 + .../src/app/core/query/lock.lookup.ts | 6 +- .../app/core/services/lock/lock.service.ts | 17 +- .../services/utilities/enum-utils.service.ts | 10 + .../description-template-editor.component.ts | 11 +- ...cription-template-type-editor.component.ts | 8 +- .../editor/dmp-blueprint-editor.component.ts | 12 +- .../lock-listing-filters.component.html | 51 +++++ .../lock-listing-filters.component.scss | 21 +++ .../filters/lock-listing-filters.component.ts | 108 +++++++++++ .../entity-locks/lock-listing.component.html | 88 +++++++++ .../entity-locks/lock-listing.component.scss | 36 ++++ .../entity-locks/lock-listing.component.ts | 176 ++++++++++++++++++ .../app/ui/admin/entity-locks/lock.module.ts | 41 ++++ .../app/ui/admin/entity-locks/lock.routing.ts | 20 ++ .../editor/language-editor.component.ts | 8 +- .../notification-template-editor.component.ts | 8 +- .../prefilling-source-editor.component.ts | 8 +- .../editor/reference-type-editor.component.ts | 8 +- .../editor/reference-editor.component.ts | 8 +- .../tenant/editor/tenant-editor.component.ts | 8 +- .../editor/description-editor.component.ts | 46 +---- .../editor/description-editor.resolver.ts | 4 +- .../description-listing-item.component.ts | 2 +- .../dmp-editor.component.ts | 53 +----- .../dmp-listing-item.component.ts | 2 +- .../ui/dmp/overview/dmp-overview.component.ts | 4 +- .../src/app/ui/sidebar/sidebar.component.ts | 1 + .../supportive-material-editor.component.ts | 7 +- dmp-frontend/src/assets/i18n/en.json | 40 +++- dmp-frontend/src/common/base/base-editor.ts | 60 +++++- .../formatting/common-formatting.module.ts | 10 +- .../formatting/pipes/lock-target-type.pipe.ts | 11 ++ 49 files changed, 1021 insertions(+), 196 deletions(-) create mode 100644 dmp-backend/core/src/main/java/eu/eudat/configurations/lock/LockConfiguration.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/configurations/lock/LockProperties.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/model/LockStatus.java create mode 100644 dmp-backend/web/src/main/resources/config/lock.yml create mode 100644 dmp-frontend/src/app/ui/admin/entity-locks/filters/lock-listing-filters.component.html create mode 100644 dmp-frontend/src/app/ui/admin/entity-locks/filters/lock-listing-filters.component.scss create mode 100644 dmp-frontend/src/app/ui/admin/entity-locks/filters/lock-listing-filters.component.ts create mode 100644 dmp-frontend/src/app/ui/admin/entity-locks/lock-listing.component.html create mode 100644 dmp-frontend/src/app/ui/admin/entity-locks/lock-listing.component.scss create mode 100644 dmp-frontend/src/app/ui/admin/entity-locks/lock-listing.component.ts create mode 100644 dmp-frontend/src/app/ui/admin/entity-locks/lock.module.ts create mode 100644 dmp-frontend/src/app/ui/admin/entity-locks/lock.routing.ts create mode 100644 dmp-frontend/src/common/formatting/pipes/lock-target-type.pipe.ts diff --git a/dmp-backend/core/src/main/java/eu/eudat/audit/AuditableAction.java b/dmp-backend/core/src/main/java/eu/eudat/audit/AuditableAction.java index 8f02ec600..1c34d4180 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/audit/AuditableAction.java +++ b/dmp-backend/core/src/main/java/eu/eudat/audit/AuditableAction.java @@ -125,6 +125,8 @@ public class AuditableAction { public static final EventId Lock_Delete = new EventId(17003, "Lock_Delete"); public static final EventId Lock_IsLocked = new EventId(17004, "Lock_IsLocked"); public static final EventId Lock_UnLocked = new EventId(17005, "Lock_UnLocked"); + public static final EventId Lock_Touched = new EventId(17006, "Lock_Touched"); + public static final EventId Lock_Locked = new EventId(17007, "Lock_Locked"); public static final EventId Deposit_GetAvailableRepositories = new EventId(18000, "Deposit_GetAvailableRepositories"); public static final EventId Deposit_GetAccessToken = new EventId(18001, "Deposit_GetAccessToken"); diff --git a/dmp-backend/core/src/main/java/eu/eudat/commons/enums/LockTargetType.java b/dmp-backend/core/src/main/java/eu/eudat/commons/enums/LockTargetType.java index 4b548c10f..5e07ab376 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/commons/enums/LockTargetType.java +++ b/dmp-backend/core/src/main/java/eu/eudat/commons/enums/LockTargetType.java @@ -7,7 +7,9 @@ import java.util.Map; public enum LockTargetType implements DatabaseEnum { Dmp((short) 0), - Decription((short) 1); + Description((short) 1), + DmpBlueprint((short) 2), + DescriptionTemplate((short) 3); private final Short value; LockTargetType(Short value) { diff --git a/dmp-backend/core/src/main/java/eu/eudat/configurations/lock/LockConfiguration.java b/dmp-backend/core/src/main/java/eu/eudat/configurations/lock/LockConfiguration.java new file mode 100644 index 000000000..48b6aa40c --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/configurations/lock/LockConfiguration.java @@ -0,0 +1,20 @@ +package eu.eudat.configurations.lock; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(LockProperties.class) +public class LockConfiguration { + private final LockProperties properties; + + @Autowired + public LockConfiguration(LockProperties properties) { + this.properties = properties; + } + + public LockProperties getProperties() { + return properties; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/configurations/lock/LockProperties.java b/dmp-backend/core/src/main/java/eu/eudat/configurations/lock/LockProperties.java new file mode 100644 index 000000000..314df9964 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/configurations/lock/LockProperties.java @@ -0,0 +1,17 @@ +package eu.eudat.configurations.lock; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "lock") +public class LockProperties { + + private Integer lockInterval; + + public Integer getLockInterval() { + return lockInterval; + } + + public void setLockInterval(Integer lockInterval) { + this.lockInterval = lockInterval; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/LockStatus.java b/dmp-backend/core/src/main/java/eu/eudat/model/LockStatus.java new file mode 100644 index 000000000..7a9707ae1 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/LockStatus.java @@ -0,0 +1,24 @@ +package eu.eudat.model; + +public class LockStatus { + + Boolean status; + + Lock lock; + + public Boolean getStatus() { + return status; + } + + public void setStatus(Boolean status) { + this.status = status; + } + + public Lock getLock() { + return lock; + } + + public void setLock(Lock lock) { + this.lock = lock; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/persist/LockPersist.java b/dmp-backend/core/src/main/java/eu/eudat/model/persist/LockPersist.java index e51949739..1b99975c2 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/model/persist/LockPersist.java +++ b/dmp-backend/core/src/main/java/eu/eudat/model/persist/LockPersist.java @@ -31,6 +31,14 @@ public class LockPersist { public static final String _hash = "hash"; + public LockPersist() { + } + + public LockPersist(UUID target, LockTargetType targetType) { + this.target = target; + this.targetType = targetType; + } + public UUID getId() { return id; } diff --git a/dmp-backend/core/src/main/java/eu/eudat/query/LockQuery.java b/dmp-backend/core/src/main/java/eu/eudat/query/LockQuery.java index 05f179793..e25127237 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/query/LockQuery.java +++ b/dmp-backend/core/src/main/java/eu/eudat/query/LockQuery.java @@ -21,6 +21,7 @@ import java.util.*; @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class LockQuery extends QueryBase { + private String like; private Collection ids; private Collection targetIds; @@ -30,8 +31,15 @@ public class LockQuery extends QueryBase { private Collection excludedIds; + private Collection userIds; + private EnumSet authorize = EnumSet.of(AuthorizationFlags.None); + public LockQuery like(String value) { + this.like = value; + return this; + } + public LockQuery ids(UUID value) { this.ids = List.of(value); return this; @@ -107,6 +115,21 @@ public class LockQuery extends QueryBase { return this; } + public LockQuery userIds(UUID value) { + this.userIds = List.of(value); + return this; + } + + public LockQuery userIds(UUID... value) { + this.userIds = Arrays.asList(value); + return this; + } + + public LockQuery userIds(Collection values) { + this.userIds = values; + return this; + } + public LockQuery authorize(EnumSet values) { this.authorize = values; return this; @@ -128,6 +151,9 @@ public class LockQuery extends QueryBase { @Override protected Predicate applyFilters(QueryContext queryContext) { List predicates = new ArrayList<>(); + if (this.like != null && !this.like.isEmpty()) { + predicates.add(queryContext.CriteriaBuilder.like(queryContext.Root.get(LockEntity._id), this.like)); + } if (this.ids != null) { CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(LockEntity._id)); for (UUID item : this.ids) @@ -158,6 +184,12 @@ public class LockQuery extends QueryBase { notInClause.value(item); predicates.add(notInClause.not()); } + if (this.userIds != null) { + CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(LockEntity._lockedBy)); + for (UUID item : this.userIds) + inClause.value(item); + predicates.add(inClause); + } if (!predicates.isEmpty()) { Predicate[] predicatesArray = predicates.toArray(new Predicate[0]); return queryContext.CriteriaBuilder.and(predicatesArray); diff --git a/dmp-backend/core/src/main/java/eu/eudat/query/lookup/LockLookup.java b/dmp-backend/core/src/main/java/eu/eudat/query/lookup/LockLookup.java index fd5cf7c5d..3d0fee51d 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/query/lookup/LockLookup.java +++ b/dmp-backend/core/src/main/java/eu/eudat/query/lookup/LockLookup.java @@ -11,6 +11,8 @@ import java.util.UUID; public class LockLookup extends Lookup { + private String like; + private List ids; private List targetIds; @@ -18,6 +20,16 @@ public class LockLookup extends Lookup { private List targetTypes; private List excludedIds; + private List userIds; + + + public String getLike() { + return like; + } + + public void setLike(String like) { + this.like = like; + } public List getIds() { return ids; @@ -51,12 +63,22 @@ public class LockLookup extends Lookup { this.targetTypes = targetTypes; } + public List getUserIds() { + return userIds; + } + + public void setUserIds(List userIds) { + this.userIds = userIds; + } + public LockQuery enrich(QueryFactory queryFactory) { LockQuery query = queryFactory.query(LockQuery.class); + if (this.like != null) query.like(this.like); if (this.ids != null) query.ids(this.ids); if (this.targetIds != null) query.targetIds(this.targetIds); if (this.targetTypes != null) query.targetTypes(this.targetTypes); if (this.excludedIds != null) query.excludedIds(this.excludedIds); + if (this.userIds != null) query.userIds(this.userIds); this.enrichCommon(query); diff --git a/dmp-backend/core/src/main/java/eu/eudat/service/lock/LockService.java b/dmp-backend/core/src/main/java/eu/eudat/service/lock/LockService.java index eadb08c56..c63092e02 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/service/lock/LockService.java +++ b/dmp-backend/core/src/main/java/eu/eudat/service/lock/LockService.java @@ -1,6 +1,8 @@ package eu.eudat.service.lock; +import eu.eudat.commons.enums.LockTargetType; import eu.eudat.model.Lock; +import eu.eudat.model.LockStatus; import eu.eudat.model.persist.LockPersist; import gr.cite.tools.exception.MyApplicationException; import gr.cite.tools.exception.MyForbiddenException; @@ -15,7 +17,11 @@ public interface LockService { Lock persist(LockPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException; - boolean isLocked(UUID target) throws InvalidApplicationException; + LockStatus isLocked(UUID target, FieldSet fields) throws InvalidApplicationException; + + void lock(UUID target, LockTargetType targetType) throws InvalidApplicationException; + + void touch(UUID target) throws InvalidApplicationException; void unlock(UUID target) throws InvalidApplicationException; diff --git a/dmp-backend/core/src/main/java/eu/eudat/service/lock/LockServiceImpl.java b/dmp-backend/core/src/main/java/eu/eudat/service/lock/LockServiceImpl.java index 29ad47987..4bddf7b21 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/service/lock/LockServiceImpl.java +++ b/dmp-backend/core/src/main/java/eu/eudat/service/lock/LockServiceImpl.java @@ -4,11 +4,14 @@ import eu.eudat.authorization.AffiliatedResource; import eu.eudat.authorization.AuthorizationFlags; import eu.eudat.authorization.Permission; import eu.eudat.authorization.authorizationcontentresolver.AuthorizationContentResolver; +import eu.eudat.commons.enums.LockTargetType; import eu.eudat.commons.scope.user.UserScope; +import eu.eudat.configurations.lock.LockProperties; import eu.eudat.convention.ConventionService; import eu.eudat.data.LockEntity; import eu.eudat.errorcode.ErrorThesaurusProperties; import eu.eudat.model.Lock; +import eu.eudat.model.LockStatus; import eu.eudat.model.builder.LockBuilder; import eu.eudat.model.deleter.LockDeleter; import eu.eudat.model.persist.LockPersist; @@ -54,18 +57,19 @@ public class LockServiceImpl implements LockService { private final MessageSource messageSource; private final ErrorThesaurusProperties errors; private final AuthorizationContentResolver authorizationContentResolver; + private final LockProperties lockProperties; @Autowired public LockServiceImpl( - EntityManager entityManager, - UserScope userScope, - AuthorizationService authorizationService, - DeleterFactory deleterFactory, - BuilderFactory builderFactory, - QueryFactory queryFactory, - ConventionService conventionService, - MessageSource messageSource, - ErrorThesaurusProperties errors, AuthorizationContentResolver authorizationContentResolver) { + EntityManager entityManager, + UserScope userScope, + AuthorizationService authorizationService, + DeleterFactory deleterFactory, + BuilderFactory builderFactory, + QueryFactory queryFactory, + ConventionService conventionService, + MessageSource messageSource, + ErrorThesaurusProperties errors, AuthorizationContentResolver authorizationContentResolver, LockProperties lockProperties) { this.entityManager = entityManager; this.userScope = userScope; this.authorizationService = authorizationService; @@ -76,6 +80,7 @@ public class LockServiceImpl implements LockService { this.messageSource = messageSource; this.errors = errors; this.authorizationContentResolver = authorizationContentResolver; + this.lockProperties = lockProperties; } public Lock persist(LockPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException { @@ -112,71 +117,57 @@ public class LockServiceImpl implements LockService { return this.builderFactory.builder(LockBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).build(BaseFieldSet.build(fields, Lock._id), data); } - public boolean isLocked(UUID target) throws InvalidApplicationException { - LockQuery query = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).targetIds(target); - if (query.count() == 1) { - LockEntity lock = query.first(); - if (lock.getLockedBy().equals(this.userScope.getUserId())) { - lock.setTouchedAt(Instant.now()); - this.entityManager.merge(lock); - this.entityManager.flush(); - return false; - } - return this.forceUnlock(target) > 0; - } else if (query.count() > 1) { - this.forceUnlock(target); - return this.isLocked(target); + public LockStatus isLocked(UUID target, FieldSet fields) throws InvalidApplicationException { + LockStatus lockStatus = new LockStatus(); + LockEntity lock = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).targetIds(target).first(); + + if (lock == null) { + lockStatus.setStatus(false); + return lockStatus; + } + + if (lock.getLockedBy().equals(this.userScope.getUserId())) lockStatus.setStatus(false); + else { + if (new Date().getTime() - Date.from(lock.getTouchedAt()).getTime() > lockProperties.getLockInterval()) { + lockStatus.setStatus(false); + this.deleteAndSave(lock.getId()); + } else lockStatus.setStatus(true); + } + + lockStatus.setLock(this.builderFactory.builder(LockBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).build(BaseFieldSet.build(fields, Lock._id), lock)); + return lockStatus; + } + + public void lock(UUID target, LockTargetType targetType) throws InvalidApplicationException { + LockEntity lock = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).targetIds(target).first(); + if (lock == null) { + this.persist(new LockPersist(target, targetType), null); + }else{ + if (!lock.getLockedBy().equals(this.userScope.getUserId())) throw new MyApplicationException("Entity is already locked"); + this.touch(target); } - return false; } - private Long forceUnlock(UUID target) throws InvalidApplicationException { + public void touch(UUID target) throws InvalidApplicationException { + LockEntity lock = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).targetIds(target).first(); - LockQuery query = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).targetIds(target); - Long availableLocks = query.count(); - long deletedLocks = 0L; - if (availableLocks > 0) { - List locks = query.collect(); - for (LockEntity lock : locks) { - if (new Date().getTime() - Date.from(lock.getTouchedAt()).getTime() > 120000) { - this.deleteAndSave(lock.getId()); - deletedLocks++; - } - } - if (deletedLocks == 0) { - LockEntity recentLock = locks.stream().max(compareByTouchedAt).get(); - for (LockEntity lock : locks) { - if (lock != recentLock) { - this.deleteAndSave(lock.getId()); - deletedLocks++; - } - } - } - } - return availableLocks - deletedLocks; + if (lock == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{target, Lock.class.getSimpleName()}, LocaleContextHolder.getLocale())); + if (!lock.getLockedBy().equals(this.userScope.getUserId())) throw new MyApplicationException("Only the user who created that lock can touch it"); + + lock.setTouchedAt(Instant.now()); + this.entityManager.merge(lock); + this.entityManager.flush(); } public void unlock(UUID target) throws InvalidApplicationException { + LockEntity lock = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).targetIds(target).first(); - LockQuery query = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).targetIds(target); - if (query.count() == 1) { - LockEntity lock = query.first(); - if (!lock.getLockedBy().equals(this.userScope.getUserId())) { - throw new InvalidApplicationException("Only the user who created that lock can delete it"); - } - this.deleteAndSave(lock.getId()); - } else if (query.count() > 1) { - List locks = query.collect(); - locks.stream().filter(lock -> lock.getLockedBy().equals(this.userScope.getUserIdSafe())).forEach(lock -> { - try { - this.deleteAndSave(lock.getId()); - } catch (InvalidApplicationException e) { - throw new RuntimeException(e); - } - }); - + if (lock == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{target, Lock.class.getSimpleName()}, LocaleContextHolder.getLocale())); + if (!lock.getLockedBy().equals(this.userScope.getUserId())) { + throw new InvalidApplicationException("Only the user who created that lock can delete it"); } + this.deleteAndSave(lock.getId()); } public void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException { diff --git a/dmp-backend/web/src/main/java/eu/eudat/controllers/LockController.java b/dmp-backend/web/src/main/java/eu/eudat/controllers/LockController.java index cb909099d..d372ec769 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/controllers/LockController.java +++ b/dmp-backend/web/src/main/java/eu/eudat/controllers/LockController.java @@ -5,6 +5,8 @@ import eu.eudat.authorization.AffiliatedResource; import eu.eudat.authorization.AuthorizationFlags; import eu.eudat.authorization.Permission; import eu.eudat.authorization.authorizationcontentresolver.AuthorizationContentResolver; +import eu.eudat.commons.enums.LockTargetType; +import eu.eudat.model.LockStatus; import gr.cite.tools.validation.ValidationFilterAnnotation; import eu.eudat.data.LockEntity; import eu.eudat.model.Lock; @@ -155,15 +157,45 @@ public class LockController { @Transactional @GetMapping("target/status/{id}") - public Boolean getLocked(@PathVariable("id") UUID targetId) throws Exception { - logger.debug(new MapLogEntry("is locked" + Lock.class.getSimpleName()).And("targetId", targetId)); + public LockStatus getLocked(@PathVariable("id") UUID targetId, FieldSet fieldSet) throws Exception { + logger.debug(new MapLogEntry("is locked" + Lock.class.getSimpleName()).And("targetId", targetId).And("fields", fieldSet)); this.authService.authorizeForce(Permission.BrowseLock); - Boolean isLocked = this.lockService.isLocked(targetId); + LockStatus lockStatus = this.lockService.isLocked(targetId, fieldSet); this.auditService.track(AuditableAction.Lock_IsLocked, Map.ofEntries( + new AbstractMap.SimpleEntry("targetId", targetId), + new AbstractMap.SimpleEntry("fields", fieldSet) + )); + return lockStatus; + } + + @Transactional + @GetMapping("target/lock/{id}/{targetType}") + public boolean lock(@PathVariable("id") UUID targetId, @PathVariable("targetType") int targetType) throws Exception { + AffiliatedResource affiliatedResourceDmp = this.authorizationContentResolver.dmpAffiliation(targetId); + AffiliatedResource affiliatedResourceDescription = this.authorizationContentResolver.descriptionAffiliation(targetId); + this.authService.authorizeAtLeastOneForce(List.of(affiliatedResourceDmp, affiliatedResourceDescription), Permission.EditLock); + + this.lockService.lock(targetId, LockTargetType.of((short) targetType)); + this.auditService.track(AuditableAction.Lock_Locked, Map.ofEntries( + new AbstractMap.SimpleEntry("targetId", targetId), + new AbstractMap.SimpleEntry("targetType", targetType) + )); + return true; + } + + @Transactional + @DeleteMapping("target/touch/{id}") + public boolean touch(@PathVariable("id") UUID targetId) throws Exception { + AffiliatedResource affiliatedResourceDmp = this.authorizationContentResolver.dmpAffiliation(targetId); + AffiliatedResource affiliatedResourceDescription = this.authorizationContentResolver.descriptionAffiliation(targetId); + this.authService.authorizeAtLeastOneForce(List.of(affiliatedResourceDmp, affiliatedResourceDescription), Permission.EditLock); + + this.lockService.touch(targetId); + this.auditService.track(AuditableAction.Lock_Touched, Map.ofEntries( new AbstractMap.SimpleEntry("targetId", targetId) )); - return isLocked; + return true; } @Transactional diff --git a/dmp-backend/web/src/main/resources/config/application.yml b/dmp-backend/web/src/main/resources/config/application.yml index 0a8821829..a4b175e10 100644 --- a/dmp-backend/web/src/main/resources/config/application.yml +++ b/dmp-backend/web/src/main/resources/config/application.yml @@ -30,5 +30,7 @@ spring: optional:classpath:config/locale.yml[.yml], optional:classpath:config/locale-${spring.profiles.active}.yml[.yml], optional:file:../config/locale-${spring.profiles.active}.yml[.yml], optional:classpath:config/public-api.yml[.yml], optional:classpath:config/public-api-${spring.profiles.active}.yml[.yml], optional:file:../config/public-api-${spring.profiles.active}.yml[.yml], optional:classpath:config/dashboard.yml[.yml], optional:classpath:config/dashboard-${spring.profiles.active}.yml[.yml], optional:file:../config/dashboard-${spring.profiles.active}.yml[.yml], - optional:classpath:config/transformer.yml[.yml], optional:classpath:config/transformer-${spring.profiles.active}.yml[.yml], optional:file:../config/transformer-${spring.profiles.active}.yml[.yml] + optional:classpath:config/transformer.yml[.yml], optional:classpath:config/transformer-${spring.profiles.active}.yml[.yml], optional:file:../config/transformer-${spring.profiles.active}.yml[.yml], + optional:classpath:config/lock.yml[.yml], optional:classpath:config/lock-${spring.profiles.active}.yml[.yml], optional:file:../config/lock-${spring.profiles.active}.yml[.yml] + diff --git a/dmp-backend/web/src/main/resources/config/lock.yml b/dmp-backend/web/src/main/resources/config/lock.yml new file mode 100644 index 000000000..a479f50f8 --- /dev/null +++ b/dmp-backend/web/src/main/resources/config/lock.yml @@ -0,0 +1,2 @@ +lock: + lockInterval: 120000 diff --git a/dmp-frontend/src/app/app-routing.module.ts b/dmp-frontend/src/app/app-routing.module.ts index 1f33204c8..b2c06f73f 100644 --- a/dmp-frontend/src/app/app-routing.module.ts +++ b/dmp-frontend/src/app/app-routing.module.ts @@ -318,6 +318,18 @@ const appRoutes: Routes = [ }) }, }, + { + path: 'entity-locks', + loadChildren: () => import('./ui/admin/entity-locks/lock.module').then(m => m.LockModule), + data: { + authContext: { + permissions: [AppPermission.ViewEntityLockPage] + }, + ...BreadcrumbService.generateRouteDataConfiguration({ + title: 'BREADCRUMBS.ENTITY-LOCKS' + }) + }, + }, { path: 'index-managment', loadChildren: () => import('./ui/admin/index-managment/index-managment.module').then(m => m.IndexManagmentModule), diff --git a/dmp-frontend/src/app/core/common/enum/lock-target-type.ts b/dmp-frontend/src/app/core/common/enum/lock-target-type.ts index 66cfe133e..c0e66f3ba 100644 --- a/dmp-frontend/src/app/core/common/enum/lock-target-type.ts +++ b/dmp-frontend/src/app/core/common/enum/lock-target-type.ts @@ -1,4 +1,6 @@ export enum LockTargetType { Dmp = 0, - Description = 1 + Description = 1, + DmpBlueprint = 2, + DescriptionTemplate= 3 } \ No newline at end of file diff --git a/dmp-frontend/src/app/core/common/enum/permission.enum.ts b/dmp-frontend/src/app/core/common/enum/permission.enum.ts index 034ce6d21..3f7ad81fa 100644 --- a/dmp-frontend/src/app/core/common/enum/permission.enum.ts +++ b/dmp-frontend/src/app/core/common/enum/permission.enum.ts @@ -46,6 +46,7 @@ export enum AppPermission { ViewMineInAppNotificationPage = "ViewMineInAppNotificationPage", ViewNotificationPage = "ViewNotificationPage", ViewPrefillingSourcePage = "ViewPrefillingSourcePage", + ViewEntityLockPage = "ViewEntityLockPage", //ReferenceType BrowseReferenceType = "BrowseReferenceType", diff --git a/dmp-frontend/src/app/core/model/lock/lock.model.ts b/dmp-frontend/src/app/core/model/lock/lock.model.ts index 3a2443caf..5dd1e8f68 100644 --- a/dmp-frontend/src/app/core/model/lock/lock.model.ts +++ b/dmp-frontend/src/app/core/model/lock/lock.model.ts @@ -19,3 +19,8 @@ export interface LockPersist extends BaseEntityPersist { target: Guid; targetType: LockTargetType; } + +export interface LockStatus { + status: Boolean; + lock: Lock; +} diff --git a/dmp-frontend/src/app/core/query/lock.lookup.ts b/dmp-frontend/src/app/core/query/lock.lookup.ts index 2cad0728c..94db04b65 100644 --- a/dmp-frontend/src/app/core/query/lock.lookup.ts +++ b/dmp-frontend/src/app/core/query/lock.lookup.ts @@ -3,19 +3,23 @@ import { Guid } from "@common/types/guid"; import { LockTargetType } from "../common/enum/lock-target-type"; export class LockLookup extends Lookup implements LockLookup { + like: string ids: Guid[]; excludedIds: Guid[]; targetIds: Guid[]; targetTypes: LockTargetType[]; + userIds: Guid[]; constructor() { super(); } } -export interface LockLookup { +export interface LockFilter { + like: string ids: Guid[]; excludedIds: Guid[]; targetIds: Guid[]; targetTypes: LockTargetType[]; + userIds: Guid[]; } \ No newline at end of file diff --git a/dmp-frontend/src/app/core/services/lock/lock.service.ts b/dmp-frontend/src/app/core/services/lock/lock.service.ts index a744475e0..e237eb337 100644 --- a/dmp-frontend/src/app/core/services/lock/lock.service.ts +++ b/dmp-frontend/src/app/core/services/lock/lock.service.ts @@ -1,6 +1,6 @@ import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Lock, LockPersist } from '@app/core/model/lock/lock.model'; +import { Lock, LockPersist, LockStatus } from '@app/core/model/lock/lock.model'; import { LockLookup } from '@app/core/query/lock.lookup'; import { QueryResult } from '@common/model/query-result'; import { FilterService } from '@common/modules/text-filter/filter-service'; @@ -9,6 +9,7 @@ import { Observable, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { ConfigurationService } from '../configuration/configuration.service'; import { BaseHttpV2Service } from '../http/base-http-v2.service'; +import { LockTargetType } from '@app/core/common/enum/lock-target-type'; @Injectable() export class LockService { @@ -50,13 +51,21 @@ export class LockService { catchError((error: any) => throwError(error))); } - checkLockStatus(targetId: Guid): Observable { - return this.http.get(`${this.apiBase}/target/status/${targetId}`) + checkLockStatus(targetId: Guid, reqFields: string[] = []): Observable { + const url = `${this.apiBase}/target/status/${targetId}`; + const options = { params: { f: reqFields } }; + + return this.http.get(url, options) + .pipe(catchError((error: any) => throwError(error))); + } + + lock(targetId: Guid, targetType: LockTargetType): Observable { + return this.http.get(`${this.apiBase}/target/lock/${targetId}/${targetType}`) .pipe(catchError((error: any) => throwError(error))); } touchLock(targetId: Guid): Observable { - return this.http.get(`${this.apiBase}/touch/${targetId}`) + return this.http.get(`${this.apiBase}/target/touch/${targetId}`) .pipe(catchError((error: any) => throwError(error))); } diff --git a/dmp-frontend/src/app/core/services/utilities/enum-utils.service.ts b/dmp-frontend/src/app/core/services/utilities/enum-utils.service.ts index 2daf1a1ff..1a0d06c9e 100644 --- a/dmp-frontend/src/app/core/services/utilities/enum-utils.service.ts +++ b/dmp-frontend/src/app/core/services/utilities/enum-utils.service.ts @@ -40,6 +40,7 @@ import { DmpBlueprintFieldCategory } from '@app/core/common/enum/dmp-blueprint-f import { DmpUserType } from '@app/core/common/enum/dmp-user-type'; import { PrefillingSourceSystemTargetType } from '@app/core/common/enum/prefilling-source-system-target-type'; import { AnnotationProtectionType } from '@app/core/common/enum/annotation-protection-type.enum'; +import { LockTargetType } from '@app/core/common/enum/lock-target-type'; @Injectable() export class EnumUtils { @@ -415,5 +416,14 @@ export class EnumUtils { default: return ''; } } + + public toLockTargetTypeString(status: LockTargetType): string { + switch (status) { + case LockTargetType.Dmp: return this.language.instant('TYPES.LOCK-TARGET-TYPE.DMP'); + case LockTargetType.Description: return this.language.instant('TYPES.LOCK-TARGET-TYPE.DESCRIPTION'); + case LockTargetType.DmpBlueprint: return this.language.instant('TYPES.LOCK-TARGET-TYPE.DMP-BLUEPRINT'); + case LockTargetType.DescriptionTemplate: return this.language.instant('TYPES.LOCK-TARGET-TYPE.DESCRIPTION-TEMPLATE'); + } + } } diff --git a/dmp-frontend/src/app/ui/admin/description-template/editor/description-template-editor.component.ts b/dmp-frontend/src/app/ui/admin/description-template/editor/description-template-editor.component.ts index 82efc836e..58733617b 100644 --- a/dmp-frontend/src/app/ui/admin/description-template/editor/description-template-editor.component.ts +++ b/dmp-frontend/src/app/ui/admin/description-template/editor/description-template-editor.component.ts @@ -39,6 +39,9 @@ import { DescriptionTemplateEditorModel, DescriptionTemplateFieldEditorModel, De import { DescriptionTemplateEditorResolver } from './description-template-editor.resolver'; import { DescriptionTemplateEditorService } from './description-template-editor.service'; import { NewEntryType, ToCEntry, ToCEntryType } from './table-of-contents/description-template-table-of-contents-entry'; +import { ConfigurationService } from '@app/core/services/configuration/configuration.service'; +import { LockService } from '@app/core/services/lock/lock.service'; +import { LockTargetType } from '@app/core/common/enum/lock-target-type'; @Component({ @@ -104,8 +107,10 @@ export class DescriptionTemplateEditorComponent extends BaseEditor { this.steps = this.stepper.steps; }); diff --git a/dmp-frontend/src/app/ui/admin/description-types/editor/description-template-type-editor.component.ts b/dmp-frontend/src/app/ui/admin/description-types/editor/description-template-type-editor.component.ts index bbba7c49f..bbcb0194e 100644 --- a/dmp-frontend/src/app/ui/admin/description-types/editor/description-template-type-editor.component.ts +++ b/dmp-frontend/src/app/ui/admin/description-types/editor/description-template-type-editor.component.ts @@ -23,6 +23,8 @@ import { map, takeUntil } from 'rxjs/operators'; import { DescriptionTemplateTypeEditorModel } from './description-template-type-editor.model'; import { DescriptionTemplateTypeEditorResolver } from './description-template-type-editor.resolver'; import { DescriptionTemplateTypeEditorService } from './description-template-type-editor.service'; +import { ConfigurationService } from '@app/core/services/configuration/configuration.service'; +import { LockService } from '@app/core/services/lock/lock.service'; @Component({ templateUrl: './description-template-type-editor.component.html', @@ -66,14 +68,16 @@ export class DescriptionTemplateTypeEditorComponent extends BaseEditor + + + + + +
+
+
+

{{'LOCK-LISTING.FILTER.TITLE' | translate}}

+ +
+ +
+
+ + {{'LOCK-LISTING.FILTER.USERS' | translate}} + + + +
+
+ + {{'LOCK-LISTING.FILTER.TARGET-TYPE' | translate}} + + {{enumUtils.toLockTargetTypeString(targetType)}} + + + +
+
+ +
+ + +
+
+
+
+ + + diff --git a/dmp-frontend/src/app/ui/admin/entity-locks/filters/lock-listing-filters.component.scss b/dmp-frontend/src/app/ui/admin/entity-locks/filters/lock-listing-filters.component.scss new file mode 100644 index 000000000..ea00a215c --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/entity-locks/filters/lock-listing-filters.component.scss @@ -0,0 +1,21 @@ +::ng-deep.mat-mdc-menu-panel { + max-width: 100% !important; + height: 100% !important; +} + +:host::ng-deep.mat-mdc-menu-content:not(:empty) { + padding-top: 0 !important; +} + + +.filter-button{ + padding-top: .6rem; + padding-bottom: .6rem; + // .mat-icon{ + // font-size: 1.5em; + // width: 1.2em; + // height: 1.2em; + // } +} + + diff --git a/dmp-frontend/src/app/ui/admin/entity-locks/filters/lock-listing-filters.component.ts b/dmp-frontend/src/app/ui/admin/entity-locks/filters/lock-listing-filters.component.ts new file mode 100644 index 000000000..d2b9a971e --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/entity-locks/filters/lock-listing-filters.component.ts @@ -0,0 +1,108 @@ +import { COMMA, ENTER } from '@angular/cdk/keycodes'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { IsActive } from '@app/core/common/enum/is-active.enum'; +import { LockTargetType } from '@app/core/common/enum/lock-target-type'; +import { LockFilter } from '@app/core/query/lock.lookup'; +import { UserService } from '@app/core/services/user/user.service'; +import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; +import { BaseComponent } from '@common/base/base.component'; +import { Guid } from '@common/types/guid'; +import { nameof } from 'ts-simple-nameof'; + +@Component({ + selector: 'app-lock-listing-filters', + templateUrl: './lock-listing-filters.component.html', + styleUrls: ['./lock-listing-filters.component.scss'] +}) +export class LockListingFiltersComponent extends BaseComponent implements OnInit, OnChanges { + + @Input() readonly filter: LockFilter; + @Output() filterChange = new EventEmitter(); + + lockTargetTypeEnumValues = this.enumUtils.getEnumValues(LockTargetType); + + readonly separatorKeysCodes: number[] = [ENTER, COMMA]; + + // * State + internalFilters: LockListingFilters = this._getEmptyFilters(); + + protected appliedFilterCount: number = 0; + constructor( + public enumUtils: EnumUtils, + public userService: UserService, + ) { super(); } + + ngOnInit() { + } + + ngOnChanges(changes: SimpleChanges): void { + const filterChange = changes[nameof(x => x.filter)]?.currentValue as LockFilter; + if (filterChange) { + this.updateFilters() + } + } + + + onSearchTermChange(searchTerm: string): void { + this.applyFilters() + } + + + protected updateFilters(): void { + this.internalFilters = this._parseToInternalFilters(this.filter); + this.appliedFilterCount = this._computeAppliedFilters(this.internalFilters); + } + + protected applyFilters(): void { + const { targetTypes, like, userIds } = this.internalFilters ?? {} + this.filterChange.emit({ + ...this.filter, + targetTypes, + like, + userIds + }) + } + + + private _parseToInternalFilters(inputFilter: LockFilter): LockListingFilters { + if (!inputFilter) { + return this._getEmptyFilters(); + } + + let { targetTypes, like, userIds } = inputFilter; + + return { + targetTypes: targetTypes, + like: like, + userIds: userIds + } + + } + + private _getEmptyFilters(): LockListingFilters { + return { + targetTypes: null, + like: null, + userIds: null + + } + } + + private _computeAppliedFilters(filters: LockListingFilters): number { + let count = 0; + // if (filters?.isActive) { + // count++ + // } + return count; + } + + clearFilters() { + this.internalFilters = this._getEmptyFilters(); + } +} + +interface LockListingFilters { + targetTypes: LockTargetType[]; + like: string; + userIds: Guid[]; +} diff --git a/dmp-frontend/src/app/ui/admin/entity-locks/lock-listing.component.html b/dmp-frontend/src/app/ui/admin/entity-locks/lock-listing.component.html new file mode 100644 index 000000000..16fad2242 --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/entity-locks/lock-listing.component.html @@ -0,0 +1,88 @@ +
+
+ +
+
+

{{'LOCK-LISTING.TITLE' | translate}}

+ + +
+
+ + + + + + + + +
+
+ + + + +
+
+ + + {{'LOCK-LISTING.FIELDS.TARGET-TYPE' | translate}}: + + {{enumUtils.toLockTargetTypeString(item.targetType) | nullifyValue}} + + +
+
+ + + {{'LOCK-LISTING.FIELDS.LOCKED-AT' | translate}}: + + {{item?.lockedAt | dateTimeFormatter : 'short' | nullifyValue}} + + +
+
+ + + {{'LOCK-LISTING.FIELDS.TOUCHED-AT' | translate}}: + + {{item?.touchedAt | dateTimeFormatter : 'short' | nullifyValue}} + + + + + +
+
+
+ + +
+
+ + + + +
+
+
\ No newline at end of file diff --git a/dmp-frontend/src/app/ui/admin/entity-locks/lock-listing.component.scss b/dmp-frontend/src/app/ui/admin/entity-locks/lock-listing.component.scss new file mode 100644 index 000000000..39bd26c9f --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/entity-locks/lock-listing.component.scss @@ -0,0 +1,36 @@ +::ng-deep datatable-header-cell { + width: fit-content !important; + padding: 0.9rem 0.7rem !important; +} + +::ng-deep .datatable-header-cell-template-wrap { + width: fit-content !important; +} + +.lock-listing { + margin-top: 1.3rem; + margin-left: 1rem; + margin-right: 2rem; + + .mat-header-row{ + background: #f3f5f8; + } + .mat-card { + margin: 16px 0; + padding: 0px; + } + + .mat-row { + cursor: pointer; + min-height: 4.5em; + } + + mat-row:hover { + background-color: #eef5f6; + } + .mat-fab-bottom-right { + float: right; + z-index: 5; + } +} + diff --git a/dmp-frontend/src/app/ui/admin/entity-locks/lock-listing.component.ts b/dmp-frontend/src/app/ui/admin/entity-locks/lock-listing.component.ts new file mode 100644 index 000000000..20e1f79a2 --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/entity-locks/lock-listing.component.ts @@ -0,0 +1,176 @@ +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AuthService } from '@app/core/services/auth/auth.service'; +import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; +import { QueryParamsService } from '@app/core/services/utilities/query-params.service'; +import { BaseListingComponent } from '@common/base/base-listing-component'; +import { PipeService } from '@common/formatting/pipe.service'; +import { DataTableDateTimeFormatPipe } from '@common/formatting/pipes/date-time-format.pipe'; +import { QueryResult } from '@common/model/query-result'; +import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component'; +import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service'; +import { ColumnDefinition, ColumnsChangedEvent, HybridListingComponent, PageLoadEvent } from '@common/modules/hybrid-listing/hybrid-listing.component'; +import { Guid } from '@common/types/guid'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { nameof } from 'ts-simple-nameof'; +import { Lock } from '@app/core/model/lock/lock.model'; +import { LockLookup } from '@app/core/query/lock.lookup'; +import { LockService } from '@app/core/services/lock/lock.service'; +import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; +import { LockTargetTypePipe } from '@common/formatting/pipes/lock-target-type.pipe'; +import { User } from '@app/core/model/user/user'; + +@Component({ + templateUrl: './lock-listing.component.html', + styleUrls: ['./lock-listing.component.scss'] +}) +export class LockListingComponent extends BaseListingComponent implements OnInit { + publish = false; + userSettingsKey = { key: 'LockListingUserSettings' }; + propertiesAvailableForOrder: ColumnDefinition[]; + + @ViewChild('actions', { static: true }) actions?: TemplateRef; + @ViewChild(HybridListingComponent, { static: true }) hybridListingComponent: HybridListingComponent; + + private readonly lookupFields: string[] = [ + nameof(x => x.id), + nameof(x => x.target), + nameof(x => x.targetType), + nameof(x => x.lockedBy), + [nameof(x => x.lockedBy), nameof(x => x.name)].join('.'), + nameof(x => x.lockedAt), + nameof(x => x.touchedAt), + nameof(x => x.hash), + ]; + + rowIdentity = x => x.id; + + constructor( + protected router: Router, + protected route: ActivatedRoute, + protected uiNotificationService: UiNotificationService, + protected httpErrorHandlingService: HttpErrorHandlingService, + protected queryParamsService: QueryParamsService, + private lockService: LockService, + public authService: AuthService, + private pipeService: PipeService, + public enumUtils: EnumUtils, + private language: TranslateService, + private dialog: MatDialog + ) { + super(router, route, uiNotificationService, httpErrorHandlingService, queryParamsService); + // Lookup setup + // Default lookup values are defined in the user settings class. + this.lookup = this.initializeLookup(); + } + + ngOnInit() { + super.ngOnInit(); + } + + protected initializeLookup(): LockLookup { + const lookup = new LockLookup(); + lookup.metadata = { countAll: true }; + lookup.page = { offset: 0, size: this.ITEMS_PER_PAGE }; + lookup.order = { items: [this.toDescSortField(nameof(x => x.touchedAt))] }; + this.updateOrderUiFields(lookup.order); + + lookup.project = { + fields: this.lookupFields + }; + + return lookup; + } + + protected setupColumns() { + this.gridColumns.push(...[{ + prop: nameof(x => x.target), + sortable: true, + languageName: 'LOCK-LISTING.FIELDS.TARGET', + }, + { + prop: nameof(x => x.targetType), + sortable: true, + languageName: 'LOCK-LISTING.FIELDS.TARGET-TYPE', + pipe: this.pipeService.getPipe(LockTargetTypePipe) + }, + { + prop: nameof(x => x.lockedBy.name), + sortable: true, + languageName: 'LOCK-LISTING.FIELDS.LOCKED-BY', + }, + { + prop: nameof(x => x.lockedAt), + sortable: true, + languageName: 'LOCK-LISTING.FIELDS.LOCKED-AT', + pipe: this.pipeService.getPipe(DataTableDateTimeFormatPipe).withFormat('short') + }, + { + prop: nameof(x => x.touchedAt), + sortable: true, + languageName: 'LOCK-LISTING.FIELDS.TOUCHED-AT', + pipe: this.pipeService.getPipe(DataTableDateTimeFormatPipe).withFormat('short') + }, + { + alwaysShown: true, + cellTemplate: this.actions, + maxWidth: 120 + } + ]); + this.propertiesAvailableForOrder = this.gridColumns.filter(x => x.sortable); + } + + // + // Listing Component functions + // + onColumnsChanged(event: ColumnsChangedEvent) { + super.onColumnsChanged(event); + this.onColumnsChangedInternal(event.properties.map(x => x.toString())); + } + + private onColumnsChangedInternal(columns: string[]) { + // Here are defined the projection fields that always requested from the api. + const fields = new Set(this.lookupFields); + this.gridColumns.map(x => x.prop) + .filter(x => !columns?.includes(x as string)) + .forEach(item => { + fields.delete(item as string) + }); + this.lookup.project = { fields: [...fields] }; + this.onPageLoad({ offset: 0 } as PageLoadEvent); + } + + protected loadListing(): Observable> { + return this.lockService.query(this.lookup); + } + + public deleteType(id: Guid) { + if (id) { + const dialogRef = this.dialog.open(ConfirmationDialogComponent, { + data: { + isDeleteConfirmation: true, + message: this.language.instant('GENERAL.CONFIRMATION-DIALOG.DELETE-ITEM'), + confirmButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CONFIRM'), + cancelButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CANCEL') + } + }); + dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => { + if (result) { + this.lockService.delete(id).pipe(takeUntil(this._destroyed)) + .subscribe( + complete => this.onCallbackSuccess(), + error => this.onCallbackError(error) + ); + } + }); + } + } + + onCallbackSuccess(): void { + this.uiNotificationService.snackBarNotification(this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-DELETE'), SnackBarNotificationLevel.Success); + this.refresh(); + } +} diff --git a/dmp-frontend/src/app/ui/admin/entity-locks/lock.module.ts b/dmp-frontend/src/app/ui/admin/entity-locks/lock.module.ts new file mode 100644 index 000000000..82abe7b30 --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/entity-locks/lock.module.ts @@ -0,0 +1,41 @@ +import { DragDropModule } from '@angular/cdk/drag-drop'; +import { NgModule } from "@angular/core"; +import { AutoCompleteModule } from "@app/library/auto-complete/auto-complete.module"; +import { CommonFormattingModule } from '@common/formatting/common-formatting.module'; +import { CommonFormsModule } from '@common/forms/common-forms.module'; +import { ConfirmationDialogModule } from '@common/modules/confirmation-dialog/confirmation-dialog.module'; +import { HybridListingModule } from "@common/modules/hybrid-listing/hybrid-listing.module"; +import { TextFilterModule } from "@common/modules/text-filter/text-filter.module"; +import { UserSettingsModule } from "@common/modules/user-settings/user-settings.module"; +import { CommonUiModule } from '@common/ui/common-ui.module'; +import { NgxDropzoneModule } from "ngx-dropzone"; +import { RichTextEditorModule } from '@app/library/rich-text-editor/rich-text-editor.module'; +import { LockRoutingModule } from './lock.routing'; +import { LockListingFiltersComponent } from './filters/lock-listing-filters.component'; +import { MatIconModule } from '@angular/material/icon'; +import { EditorModule } from '@tinymce/tinymce-angular'; +import { LockListingComponent } from './lock-listing.component'; + +@NgModule({ + imports: [ + CommonUiModule, + CommonFormsModule, + ConfirmationDialogModule, + LockRoutingModule, + NgxDropzoneModule, + DragDropModule, + AutoCompleteModule, + HybridListingModule, + TextFilterModule, + UserSettingsModule, + CommonFormattingModule, + RichTextEditorModule, + MatIconModule, + EditorModule + ], + declarations: [ + LockListingComponent, + LockListingFiltersComponent + ] +}) +export class LockModule { } diff --git a/dmp-frontend/src/app/ui/admin/entity-locks/lock.routing.ts b/dmp-frontend/src/app/ui/admin/entity-locks/lock.routing.ts new file mode 100644 index 000000000..b206d77be --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/entity-locks/lock.routing.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AuthGuard } from '@app/core/auth-guard.service'; +import { LockListingComponent } from './lock-listing.component'; + +const routes: Routes = [ + { + path: '', + component: LockListingComponent, + canActivate: [AuthGuard] + }, + { path: '**', loadChildren: () => import('@common/modules/page-not-found/page-not-found.module').then(m => m.PageNotFoundModule) }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [] +}) +export class LockRoutingModule { } diff --git a/dmp-frontend/src/app/ui/admin/language/editor/language-editor.component.ts b/dmp-frontend/src/app/ui/admin/language/editor/language-editor.component.ts index d6e12f228..94c975644 100644 --- a/dmp-frontend/src/app/ui/admin/language/editor/language-editor.component.ts +++ b/dmp-frontend/src/app/ui/admin/language/editor/language-editor.component.ts @@ -29,6 +29,8 @@ import { LanguageEditorService } from './language-editor.service'; import { LanguageEditorModel } from './language-editor.model'; import { LanguageHttpService } from '@app/core/services/language/language.http.service'; import { MatCheckboxChange } from '@angular/material/checkbox'; +import { ConfigurationService } from '@app/core/services/configuration/configuration.service'; +import { LockService } from '@app/core/services/lock/lock.service'; @Component({ @@ -75,8 +77,10 @@ export class LanguageEditorComponent extends BaseEditor protected datePipe: DatePipe, protected route: ActivatedRoute, protected queryParamsService: QueryParamsService, + protected lockService: LockService, + protected authService: AuthService, + protected configurationService: ConfigurationService, // Rest dependencies. Inject any other needed deps here: - public authService: AuthService, public enumUtils: EnumUtils, private tenantService: TenantService, private logger: LoggingService, @@ -83,7 +87,7 @@ export class TenantEditorComponent extends BaseEditor private fileUtils: FileUtils, private matomoService: MatomoService ) { - super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService); + super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService, lockService, authService, configurationService); } ngOnInit(): void { diff --git a/dmp-frontend/src/app/ui/description/editor/description-editor.component.ts b/dmp-frontend/src/app/ui/description/editor/description-editor.component.ts index 0e73fe8be..26165db66 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-editor.component.ts +++ b/dmp-frontend/src/app/ui/description/editor/description-editor.component.ts @@ -40,6 +40,8 @@ import { ToCEntry } from './table-of-contents/models/toc-entry'; import { ToCEntryType } from './table-of-contents/models/toc-entry-type.enum'; import { TableOfContentsComponent } from './table-of-contents/table-of-contents.component'; import { FormValidationErrorsDialogComponent } from '@common/forms/form-validation-errors-dialog/form-validation-errors-dialog.component'; +import { ConfigurationService } from '@app/core/services/configuration/configuration.service'; +import { LockTargetType } from '@app/core/common/enum/lock-target-type'; @Component({ selector: 'app-description-editor-component', @@ -90,8 +92,10 @@ export class DescriptionEditorComponent extends BaseEditor { - this.lockStatus = lockStatus; - if (this.item.status === DescriptionStatus.Finalized || lockStatus) { - this.formGroup.disable(); - this.viewOnly = true; - } - if (lockStatus) { - this.dialog.open(PopupNotificationDialogComponent, { - data: { - title: this.language.instant('DATASET-WIZARD.LOCKED.TITLE'), - message: this.language.instant('DATASET-WIZARD.LOCKED.MESSAGE') - }, maxWidth: '30em' - }); - } - - if (!lockStatus && !isNullOrUndefined(this.authService.currentAccountIsAuthenticated())) { - //TODO: lock it. - // const lockedBy: UserInfoListingModel = { - // email: this.authService.getUserProfileEmail(), - // id: this.authService.userId()?.toString(), - // name: this.authService.getPrincipalName(), - // role: 0 //TODO - // //role: this.authService.getRoles()?.at(0) - // } - // this.lock = new LockModel(data.id, lockedBy); - - // this.lockService.createOrUpdate(this.lock).pipe(takeUntil(this._destroyed)).subscribe(async result => { - // this.lock.id = Guid.parse(result); - // interval(this.configurationService.lockInterval).pipe(takeUntil(this._destroyed)).subscribe(() => this.pumpLock()); - // }); - } - // this.loadDescriptionProfiles(); - // this.registerFormListeners(); - }); + this.checkLock(this.item.id, LockTargetType.Description); + } else if (dmpId != null && dmpSectionId != null) { this.isNew = true; const dialogRef = this.dialog.open(PrefillDescriptionDialogComponent, { diff --git a/dmp-frontend/src/app/ui/description/editor/description-editor.resolver.ts b/dmp-frontend/src/app/ui/description/editor/description-editor.resolver.ts index 89240649a..41b49d729 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-editor.resolver.ts +++ b/dmp-frontend/src/app/ui/description/editor/description-editor.resolver.ts @@ -13,7 +13,7 @@ import { DmpService } from '@app/core/services/dmp/dmp.service'; import { BreadcrumbService } from '@app/ui/misc/breadcrumb/breadcrumb.service'; import { BaseEditorResolver } from '@common/base/base-editor.resolver'; import { Guid } from '@common/types/guid'; -import { map, takeUntil, tap } from 'rxjs/operators'; +import { concatMap, map, takeUntil, tap } from 'rxjs/operators'; import { nameof } from 'ts-simple-nameof'; @Injectable() @@ -176,7 +176,7 @@ export class DescriptionEditorResolver extends BaseEditorResolver { return description; })); } else if (copyDmpId != null && id != null && dmpSectionId != null) { - return this.dmpService.getSingle(Guid.parse(copyDmpId), DescriptionEditorResolver.dmpLookupFields()).pipe(tap(x => this.breadcrumbService.addIdResolvedValue(x.id?.toString(), x.label)), takeUntil(this._destroyed), map(dmp => { + return this.dmpService.getSingle(Guid.parse(copyDmpId), DescriptionEditorResolver.dmpLookupFields()).pipe(tap(x => this.breadcrumbService.addIdResolvedValue(x.id?.toString(), x.label)), takeUntil(this._destroyed), concatMap(dmp => { return this.descriptionService.getSingle(Guid.parse(id), DescriptionEditorResolver.cloneLookupFields()).pipe(tap(x => this.breadcrumbService.addIdResolvedValue(x.id?.toString(), x.label)), takeUntil(this._destroyed), map(description => { description.dmp = dmp; diff --git a/dmp-frontend/src/app/ui/description/listing/listing-item/description-listing-item.component.ts b/dmp-frontend/src/app/ui/description/listing/listing-item/description-listing-item.component.ts index 831b7767d..539ad3045 100644 --- a/dmp-frontend/src/app/ui/description/listing/listing-item/description-listing-item.component.ts +++ b/dmp-frontend/src/app/ui/description/listing/listing-item/description-listing-item.component.ts @@ -194,7 +194,7 @@ export class DescriptionListingItemComponent extends BaseComponent implements On deleteClicked(id: Guid) { this.lockService.checkLockStatus(id).pipe(takeUntil(this._destroyed)) .subscribe(lockStatus => { - if (!lockStatus) { + if (!lockStatus.status) { this.openDeleteDialog(id); } else { this.openLockedByUserDialog(); diff --git a/dmp-frontend/src/app/ui/dmp/dmp-editor-blueprint/dmp-editor.component.ts b/dmp-frontend/src/app/ui/dmp/dmp-editor-blueprint/dmp-editor.component.ts index fb395fe00..713f16426 100644 --- a/dmp-frontend/src/app/ui/dmp/dmp-editor-blueprint/dmp-editor.component.ts +++ b/dmp-frontend/src/app/ui/dmp/dmp-editor-blueprint/dmp-editor.component.ts @@ -21,7 +21,6 @@ import { DescriptionSectionPermissionResolver } from '@app/core/model/descriptio import { DmpBlueprint } from '@app/core/model/dmp-blueprint/dmp-blueprint'; import { Dmp, DmpPersist } from '@app/core/model/dmp/dmp'; import { LanguageInfo } from '@app/core/model/language-info'; -import { LockPersist } from '@app/core/model/lock/lock.model'; import { AuthService } from '@app/core/services/auth/auth.service'; import { ConfigurationService } from '@app/core/services/configuration/configuration.service'; import { LanguageInfoService } from '@app/core/services/culture/language-info-service'; @@ -64,11 +63,8 @@ export class DmpEditorComponent extends BaseEditor implemen isDeleted = false; item: Dmp; selectedBlueprint: DmpBlueprint; - lockStatus: Boolean = false; step: number = 0; - - //Enums descriptionStatusEnum = DescriptionStatus; dmpBlueprintSectionFieldCategoryEnum = DmpBlueprintFieldCategory; @@ -135,14 +131,14 @@ export class DmpEditorComponent extends BaseEditor implemen protected datePipe: DatePipe, protected route: ActivatedRoute, protected queryParamsService: QueryParamsService, + protected lockService: LockService, + protected authService: AuthService, + protected configurationService: ConfigurationService, // Rest dependencies. Inject any other needed deps here: - public authService: AuthService, private dmpService: DmpService, private logger: LoggingService, public dmpBlueprintService: DmpBlueprintService, private matomoService: MatomoService, - private lockService: LockService, - private configurationService: ConfigurationService, // public visibilityRulesService: VisibilityRulesService, private languageInfoService: LanguageInfoService, public enumUtils: EnumUtils, @@ -151,7 +147,7 @@ export class DmpEditorComponent extends BaseEditor implemen public descriptionService: DescriptionService ) { - super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService); + super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService, lockService, authService, configurationService); } ngOnInit(): void { @@ -202,7 +198,7 @@ export class DmpEditorComponent extends BaseEditor implemen } if (this.item.id != null) { - this.checkLock(this.item.id); + this.checkLock(this.item.id, LockTargetType.Dmp); } } catch (error) { this.logger.error('Could not parse Dmp item: ' + data + error); @@ -471,45 +467,6 @@ export class DmpEditorComponent extends BaseEditor implemen // }); } - // - // - // Lock - // - // - private checkLock(itemId: Guid) { - if (itemId != null) { - this.isNew = false; - // check if locked. - this.lockService.checkLockStatus(itemId).pipe(takeUntil(this._destroyed)).subscribe(lockStatus => { - this.lockStatus = lockStatus; - if (lockStatus) { - this.formGroup.disable(); - this.dialog.open(PopupNotificationDialogComponent, { - data: { - title: this.language.instant('DATASET-WIZARD.LOCKED.TITLE'), - message: this.language.instant('DATASET-WIZARD.LOCKED.MESSAGE') - }, maxWidth: '30em' - }); - } - - if (!lockStatus && !isNullOrUndefined(this.authService.currentAccountIsAuthenticated())) { - // lock it. - const lockPersist: LockPersist = { - target: itemId, - targetType: LockTargetType.Dmp, - } - this.lockService.persist(lockPersist).pipe(takeUntil(this._destroyed)).subscribe(async result => { - interval(this.configurationService.lockInterval).pipe(takeUntil(this._destroyed)).subscribe(() => this.touchLock(itemId)); - }); - } - }); - } - } - - private touchLock(targetId: Guid) { - this.lockService.checkLockStatus(targetId).pipe(takeUntil(this._destroyed)).subscribe(async result => { }); - } - // // // Misc diff --git a/dmp-frontend/src/app/ui/dmp/listing/listing-item/dmp-listing-item.component.ts b/dmp-frontend/src/app/ui/dmp/listing/listing-item/dmp-listing-item.component.ts index 52f85c1cf..cc13d266e 100644 --- a/dmp-frontend/src/app/ui/dmp/listing/listing-item/dmp-listing-item.component.ts +++ b/dmp-frontend/src/app/ui/dmp/listing/listing-item/dmp-listing-item.component.ts @@ -156,7 +156,7 @@ export class DmpListingItemComponent extends BaseComponent implements OnInit { deleteClicked(id: Guid) { this.lockService.checkLockStatus(Guid.parse(id.toString())).pipe(takeUntil(this._destroyed)) .subscribe(lockStatus => { - if (!lockStatus) { + if (!lockStatus.status) { this.openDeleteDialog(id); } else { this.openLockedByUserDialog(); diff --git a/dmp-frontend/src/app/ui/dmp/overview/dmp-overview.component.ts b/dmp-frontend/src/app/ui/dmp/overview/dmp-overview.component.ts index 235390a54..6a8a44336 100644 --- a/dmp-frontend/src/app/ui/dmp/overview/dmp-overview.component.ts +++ b/dmp-frontend/src/app/ui/dmp/overview/dmp-overview.component.ts @@ -709,8 +709,8 @@ export class DmpOverviewComponent extends BaseComponent implements OnInit { checkLockStatus(id: Guid) { this.lockService.checkLockStatus(Guid.parse(id.toString())).pipe(takeUntil(this._destroyed)) .subscribe(lockStatus => { - this.lockStatus = lockStatus - if (lockStatus) { + this.lockStatus = lockStatus.status; + if (this.lockStatus) { this.dialog.open(PopupNotificationDialogComponent, { data: { title: this.language.instant('DMP-OVERVIEW.LOCKED-DIALOG.TITLE'), diff --git a/dmp-frontend/src/app/ui/sidebar/sidebar.component.ts b/dmp-frontend/src/app/ui/sidebar/sidebar.component.ts index 819df99fc..198bbba44 100644 --- a/dmp-frontend/src/app/ui/sidebar/sidebar.component.ts +++ b/dmp-frontend/src/app/ui/sidebar/sidebar.component.ts @@ -51,6 +51,7 @@ export const ADMIN_ROUTES: RouteInfo[] = [ { path: '/dmp-blueprints', title: 'SIDE-BAR.DMP-BLUEPRINTS', icon: 'library_books' }, { path: '/description-templates', title: 'SIDE-BAR.DESCRIPTION-TEMPLATES', icon: 'description' }, { path: '/description-template-type', title: 'SIDE-BAR.DESCRIPTION-TEMPLATE-TYPES', icon: 'stack' }, + { path: '/entity-locks', title: 'SIDE-BAR.ENTITY-LOCKS', icon: 'build'}, { path: '/references', title: 'SIDE-BAR.REFERENCES', icon: 'dataset_linked' }, { path: '/reference-type', title: 'SIDE-BAR.REFERENCE-TYPES', icon: 'add_link' }, { path: '/prefilling-sources', title: 'SIDE-BAR.PREFILLING-SOURCES', icon: 'add_link' }, diff --git a/dmp-frontend/src/app/ui/supportive-material-editor/supportive-material-editor.component.ts b/dmp-frontend/src/app/ui/supportive-material-editor/supportive-material-editor.component.ts index 6b6801e18..2f2341518 100644 --- a/dmp-frontend/src/app/ui/supportive-material-editor/supportive-material-editor.component.ts +++ b/dmp-frontend/src/app/ui/supportive-material-editor/supportive-material-editor.component.ts @@ -34,6 +34,7 @@ import { LanguageHttpService } from '@app/core/services/language/language.http.s import { nameof } from 'ts-simple-nameof'; import { Language } from '@app/core/model/language/language'; import { LanguageLookup } from '@app/core/query/language.lookup'; +import { LockService } from '@app/core/services/lock/lock.service'; @Component({ @@ -64,8 +65,10 @@ export class SupportiveMaterialEditorComponent extends BaseEditor(x => x.hash), ]; } + + // + // + // Lock + // + // + protected checkLock(itemId: Guid, targetType: LockTargetType) { + if (itemId != null) { + this.isNew = false; + // check if locked. + this.lockService.checkLockStatus(itemId).pipe(takeUntil(this._destroyed)).subscribe(lockStatus => { + this.isLocked = lockStatus.status; + if (this.isLocked) { + this.formGroup.disable(); + this.dialog.open(PopupNotificationDialogComponent, { + data: { + title: this.language.instant('DATASET-WIZARD.LOCKED.TITLE'), + message: this.language.instant('DATASET-WIZARD.LOCKED.MESSAGE') + }, maxWidth: '30em' + }); + } + + if (!this.isLocked && !isNullOrUndefined(this.authService.currentAccountIsAuthenticated())) { + // lock it. + this.lockService.lock(itemId, targetType).pipe(takeUntil(this._destroyed)).subscribe(async result => { + this.isLocked = true; + interval(this.configurationService.lockInterval).pipe(takeUntil(this._destroyed)).subscribe(() => this.touchLock(itemId)); + }); + } + }); + } + } + + private unlockTarget(targetId: Guid) { + this.lockService.unlockTarget(targetId).pipe(takeUntil(this._destroyed)).subscribe(async result => { }); + } + + private touchLock(targetId: Guid) { + this.lockService.touchLock(targetId).pipe(takeUntil(this._destroyed)).subscribe(async result => { }); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + if(this.isLocked) this.unlockTarget(this.editorModel.id); + } } diff --git a/dmp-frontend/src/common/formatting/common-formatting.module.ts b/dmp-frontend/src/common/formatting/common-formatting.module.ts index 7f85e399c..eea51fd69 100644 --- a/dmp-frontend/src/common/formatting/common-formatting.module.ts +++ b/dmp-frontend/src/common/formatting/common-formatting.module.ts @@ -13,6 +13,7 @@ import { NotificationTrackingProcessPipe } from './pipes/notification-tracking-p import { NotificationTrackingStatePipe } from './pipes/notification-tracking-state.pipe'; import { NotificationTypePipe } from './pipes/notification-type.pipe'; import { ReferenceSourceTypePipe } from './pipes/reference-source-type.pipe'; +import { LockTargetTypePipe } from './pipes/lock-target-type.pipe'; // // @@ -37,7 +38,8 @@ import { ReferenceSourceTypePipe } from './pipes/reference-source-type.pipe'; NotificationContactTypePipe, NotificationNotifyStatePipe, NotificationTrackingProcessPipe, - NotificationTrackingStatePipe + NotificationTrackingStatePipe, + LockTargetTypePipe ], exports: [ DateFormatPipe, @@ -56,7 +58,8 @@ import { ReferenceSourceTypePipe } from './pipes/reference-source-type.pipe'; NotificationContactTypePipe, NotificationNotifyStatePipe, NotificationTrackingProcessPipe, - NotificationTrackingStatePipe + NotificationTrackingStatePipe, + LockTargetTypePipe ], providers: [ DateFormatPipe, @@ -75,7 +78,8 @@ import { ReferenceSourceTypePipe } from './pipes/reference-source-type.pipe'; NotificationContactTypePipe, NotificationNotifyStatePipe, NotificationTrackingProcessPipe, - NotificationTrackingStatePipe + NotificationTrackingStatePipe, + LockTargetTypePipe ] }) export class CommonFormattingModule { } diff --git a/dmp-frontend/src/common/formatting/pipes/lock-target-type.pipe.ts b/dmp-frontend/src/common/formatting/pipes/lock-target-type.pipe.ts new file mode 100644 index 000000000..0976dd458 --- /dev/null +++ b/dmp-frontend/src/common/formatting/pipes/lock-target-type.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; + +@Pipe({ name: 'LockTargetTypeFormat' }) +export class LockTargetTypePipe implements PipeTransform { + constructor(private enumUtils: EnumUtils) { } + + public transform(value): any { + return this.enumUtils.toLockTargetTypeString(value); + } +} \ No newline at end of file