From 18452725bcd288e382ee9f7bc5e53b2ab894c0e0 Mon Sep 17 00:00:00 2001 From: amentis Date: Wed, 22 Nov 2023 18:01:58 +0200 Subject: [PATCH] add Tenant Entity --- .../java/eu/eudat/audit/AuditableAction.java | 5 + .../eu/eudat/authorization/Permission.java | 5 + .../main/java/eu/eudat/data/TenantEntity.java | 112 +++++++++++ .../src/main/java/eu/eudat/model/Tenant.java | 108 ++++++++++ .../eu/eudat/model/builder/TenantBuilder.java | 56 ++++++ .../eudat/model/censorship/TenantCensor.java | 42 ++++ .../eu/eudat/model/deleter/TenantDeleter.java | 79 ++++++++ .../eu/eudat/model/persist/TenantPersist.java | 80 ++++++++ .../main/java/eu/eudat/query/TenantQuery.java | 161 +++++++++++++++ .../eu/eudat/query/lookup/TenantLookup.java | 61 ++++++ .../eudat/service/tenant/TenantService.java | 19 ++ .../service/tenant/TenantServiceImpl.java | 129 ++++++++++++ .../controllers/v2/TenantController.java | 135 +++++++++++++ .../src/main/resources/config/permissions.yml | 21 ++ dmp-frontend/src/app/app-routing.module.ts | 10 +- .../app/core/common/enum/permission.enum.ts | 5 + .../src/app/core/core-service.module.ts | 4 +- .../src/app/core/model/tenant/tenant.ts | 17 ++ .../src/app/core/query/tenant.lookup.ts | 21 ++ .../core/services/tenant/tenant.service.ts | 94 +++++++++ .../editor/reference-type-editor.resolver.ts | 4 - .../editor/tenant-editor.component.html | 71 +++++++ .../editor/tenant-editor.component.scss | 116 +++++++++++ .../tenant/editor/tenant-editor.component.ts | 185 ++++++++++++++++++ .../tenant/editor/tenant-editor.model.ts | 58 ++++++ .../tenant/editor/tenant-editor.resolver.ts | 44 +++++ .../tenant/editor/tenant-editor.service.ts | 15 ++ .../tenant-listing-filters.component.html | 36 ++++ .../tenant-listing-filters.component.scss | 25 +++ .../tenant-listing-filters.component.ts | 94 +++++++++ .../listing/tenant-listing.component.html | 108 ++++++++++ .../listing/tenant-listing.component.scss | 60 ++++++ .../listing/tenant-listing.component.ts | 170 ++++++++++++++++ .../src/app/ui/admin/tenant/tenant.module.ts | 41 ++++ .../src/app/ui/admin/tenant/tenant.routing.ts | 58 ++++++ .../src/app/ui/sidebar/sidebar.component.ts | 1 + dmp-frontend/src/assets/i18n/en.json | 55 +++++- 37 files changed, 2296 insertions(+), 9 deletions(-) create mode 100644 dmp-backend/core/src/main/java/eu/eudat/data/TenantEntity.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/model/Tenant.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/model/builder/TenantBuilder.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/model/censorship/TenantCensor.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/model/deleter/TenantDeleter.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/model/persist/TenantPersist.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/query/TenantQuery.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/query/lookup/TenantLookup.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/service/tenant/TenantService.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/service/tenant/TenantServiceImpl.java create mode 100644 dmp-backend/web/src/main/java/eu/eudat/controllers/v2/TenantController.java create mode 100644 dmp-frontend/src/app/core/model/tenant/tenant.ts create mode 100644 dmp-frontend/src/app/core/query/tenant.lookup.ts create mode 100644 dmp-frontend/src/app/core/services/tenant/tenant.service.ts create mode 100644 dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.component.html create mode 100644 dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.component.scss create mode 100644 dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.component.ts create mode 100644 dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.model.ts create mode 100644 dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.resolver.ts create mode 100644 dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.service.ts create mode 100644 dmp-frontend/src/app/ui/admin/tenant/listing/filters/tenant-listing-filters.component.html create mode 100644 dmp-frontend/src/app/ui/admin/tenant/listing/filters/tenant-listing-filters.component.scss create mode 100644 dmp-frontend/src/app/ui/admin/tenant/listing/filters/tenant-listing-filters.component.ts create mode 100644 dmp-frontend/src/app/ui/admin/tenant/listing/tenant-listing.component.html create mode 100644 dmp-frontend/src/app/ui/admin/tenant/listing/tenant-listing.component.scss create mode 100644 dmp-frontend/src/app/ui/admin/tenant/listing/tenant-listing.component.ts create mode 100644 dmp-frontend/src/app/ui/admin/tenant/tenant.module.ts create mode 100644 dmp-frontend/src/app/ui/admin/tenant/tenant.routing.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 65c223b82..2ec15c6f1 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 @@ -74,6 +74,11 @@ public class AuditableAction { public static final EventId User_LookupByEmail = new EventId(11004, "User_LookupByEmail"); public static final EventId User_ExportCsv = new EventId(11005, "User_ExportCsv"); public static final EventId User_PersistRoles = new EventId(11004, "User_PersistRoles"); + + public static final EventId Tenant_Query = new EventId(12000, "Tenant_Query"); + public static final EventId Tenant_Lookup = new EventId(12001, "Tenant_Lookup"); + public static final EventId Tenant_Persist = new EventId(12002, "Tenant_Persist"); + public static final EventId Tenant_Delete = new EventId(12003, "Tenant_Delete"); } diff --git a/dmp-backend/core/src/main/java/eu/eudat/authorization/Permission.java b/dmp-backend/core/src/main/java/eu/eudat/authorization/Permission.java index d5ecf6643..82d82a3a3 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/authorization/Permission.java +++ b/dmp-backend/core/src/main/java/eu/eudat/authorization/Permission.java @@ -139,5 +139,10 @@ public final class Permission { public static String EditReferenceType= "EditReferenceType"; public static String DeleteReferenceType = "DeleteReferenceType"; + //Tenant + public static String BrowseTenant = "BrowseTenant"; + public static String EditTenant= "EditTenant"; + public static String DeleteTenant = "DeleteTenant"; + } diff --git a/dmp-backend/core/src/main/java/eu/eudat/data/TenantEntity.java b/dmp-backend/core/src/main/java/eu/eudat/data/TenantEntity.java new file mode 100644 index 000000000..3c3314365 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/data/TenantEntity.java @@ -0,0 +1,112 @@ +package eu.eudat.data; + + +import eu.eudat.commons.enums.IsActive; +import eu.eudat.data.converters.enums.IsActiveConverter; +import jakarta.persistence.*; + +import java.time.Instant; +import java.util.UUID; + +@Entity +@Table(name = "\"Tenant\"") +public class TenantEntity { + + @Id + @Column(name = "id", columnDefinition = "uuid", updatable = false, nullable = false) + private UUID id; + public final static String _id = "id"; + + @Column(name = "code", length = 200, nullable = false) + private String code; + public final static String _code = "code"; + + @Column(name = "name", length = 500, nullable = false) + private String name; + public final static String _name = "name"; + + @Column(name = "description", nullable = false) + private String description; + public final static String _description = "description"; + + @Column(name = "is_active", length = 20, nullable = false) + @Convert(converter = IsActiveConverter.class) + private IsActive isActive; + public final static String _isActive = "isActive"; + + @Column(name = "config") + private String config; + public final static String _config = "config"; + + @Column(name = "created_at", nullable = false) + private Instant createdAt; + public final static String _createdAt = "createdAt"; + + @Column(name = "updated_at", nullable = false) + private Instant updatedAt; + public final static String _updatedAt = "updatedAt"; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public IsActive getIsActive() { + return isActive; + } + + public void setIsActive(IsActive isActive) { + this.isActive = isActive; + } + + public String getConfig() { + return config; + } + + public void setConfig(String config) { + this.config = config; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/Tenant.java b/dmp-backend/core/src/main/java/eu/eudat/model/Tenant.java new file mode 100644 index 000000000..46b589b72 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/Tenant.java @@ -0,0 +1,108 @@ +package eu.eudat.model; + +import eu.eudat.commons.enums.IsActive; + +import java.time.Instant; +import java.util.UUID; + + +public class Tenant { + + private UUID id; + public final static String _id = "id"; + + private String code; + public final static String _code = "code"; + + private String name; + public final static String _name = "name"; + + private String description; + public final static String _description = "description"; + + private IsActive isActive; + public final static String _isActive = "isActive"; + + private String config; + public final static String _config = "config"; + + private Instant createdAt; + public final static String _createdAt = "createdAt"; + + private Instant updatedAt; + public final static String _updatedAt = "updatedAt"; + + public final static String _hash = "hash"; + private String hash; + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public IsActive getIsActive() { + return isActive; + } + + public void setIsActive(IsActive isActive) { + this.isActive = isActive; + } + + public String getConfig() { + return config; + } + + public void setConfig(String config) { + this.config = config; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + public String getHash() { + return hash; + } + + public void setHash(String hash) { + this.hash = hash; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/builder/TenantBuilder.java b/dmp-backend/core/src/main/java/eu/eudat/model/builder/TenantBuilder.java new file mode 100644 index 000000000..6475bc955 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/builder/TenantBuilder.java @@ -0,0 +1,56 @@ +package eu.eudat.model.builder; + +import eu.eudat.authorization.AuthorizationFlags; +import eu.eudat.convention.ConventionService; +import eu.eudat.data.TenantEntity; +import eu.eudat.model.Tenant; +import gr.cite.tools.exception.MyApplicationException; +import gr.cite.tools.fieldset.FieldSet; +import gr.cite.tools.logging.DataLogEntry; +import gr.cite.tools.logging.LoggerService; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.*; + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class TenantBuilder extends BaseBuilder { + + private EnumSet authorize = EnumSet.of(AuthorizationFlags.None); + @Autowired + public TenantBuilder(ConventionService conventionService) { + super(conventionService, new LoggerService(LoggerFactory.getLogger(TenantBuilder.class))); + } + + public TenantBuilder authorize(EnumSet values){ + this.authorize = values; + return this; + } + @Override + public List build(FieldSet fields, List data) throws MyApplicationException { + this.logger.debug("building for {} items requesting {} fields", Optional.ofNullable(data).map(List::size).orElse(0),Optional.ofNullable(fields).map(FieldSet::getFields).map(Set::size) .orElse(0)); + this.logger.trace(new DataLogEntry("requested fields",fields)); + if(fields == null || data == null || fields.isEmpty()) return new ArrayList<>(); + + List models = new ArrayList<>(); + for(TenantEntity d : data){ + Tenant m = new Tenant(); + if(fields.hasField(this.asIndexer(Tenant._id))) m.setId(d.getId()); + if(fields.hasField(this.asIndexer(Tenant._code))) m.setCode(d.getCode()); + if(fields.hasField(this.asIndexer(Tenant._name))) m.setName(d.getName()); + if(fields.hasField(this.asIndexer(Tenant._description))) m.setDescription(d.getDescription()); + if(fields.hasField(this.asIndexer(Tenant._isActive))) m.setIsActive(d.getIsActive()); + if(fields.hasField(this.asIndexer(Tenant._config))) m.setConfig(d.getConfig()); + if(fields.hasField(this.asIndexer(Tenant._createdAt))) m.setCreatedAt(d.getCreatedAt()); + if(fields.hasField(this.asIndexer(Tenant._updatedAt))) m.setUpdatedAt(d.getUpdatedAt()); + if(fields.hasField(this.asIndexer(Tenant._hash))) m.setHash(this.hashValue(d.getUpdatedAt())); + models.add(m); + } + this.logger.debug("build {} items",Optional.of(models).map(List::size).orElse(0)); + return models; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/censorship/TenantCensor.java b/dmp-backend/core/src/main/java/eu/eudat/model/censorship/TenantCensor.java new file mode 100644 index 000000000..690c819f0 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/censorship/TenantCensor.java @@ -0,0 +1,42 @@ +package eu.eudat.model.censorship; + +import eu.eudat.authorization.Permission; +import eu.eudat.convention.ConventionService; +import gr.cite.commons.web.authz.service.AuthorizationService; +import gr.cite.tools.data.censor.CensorFactory; +import gr.cite.tools.fieldset.FieldSet; +import gr.cite.tools.logging.DataLogEntry; +import gr.cite.tools.logging.LoggerService; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class TenantCensor extends BaseCensor { + + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(TenantCensor.class)); + + protected final AuthorizationService authService; + protected final CensorFactory censorFactory; + + public TenantCensor(ConventionService conventionService, + AuthorizationService authService, + CensorFactory censorFactory) { + super(conventionService); + this.authService = authService; + this.censorFactory = censorFactory; + } + + public void censor(FieldSet fields, UUID userId) { + logger.debug(new DataLogEntry("censoring fields", fields)); + if (fields == null || fields.isEmpty()) + return; + + this.authService.authorizeForce(Permission.BrowseTenant); + } + +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/deleter/TenantDeleter.java b/dmp-backend/core/src/main/java/eu/eudat/model/deleter/TenantDeleter.java new file mode 100644 index 000000000..8d5f90e76 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/deleter/TenantDeleter.java @@ -0,0 +1,79 @@ +package eu.eudat.model.deleter; + +import eu.eudat.commons.enums.IsActive; +import eu.eudat.data.TenantEntity; +import eu.eudat.query.TenantQuery; +import gr.cite.tools.data.deleter.Deleter; +import gr.cite.tools.data.deleter.DeleterFactory; +import gr.cite.tools.data.query.QueryFactory; +import gr.cite.tools.logging.LoggerService; +import gr.cite.tools.logging.MapLogEntry; +import jakarta.persistence.EntityManager; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import javax.management.InvalidApplicationException; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class TenantDeleter implements Deleter { + + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(TenantDeleter.class)); + + private final EntityManager entityManager; + + protected final QueryFactory queryFactory; + + protected final DeleterFactory deleterFactory; + + @Autowired + public TenantDeleter( + EntityManager entityManager, + QueryFactory queryFactory, + DeleterFactory deleterFactory + ) { + this.entityManager = entityManager; + this.queryFactory = queryFactory; + this.deleterFactory = deleterFactory; + } + + public void deleteAndSaveByIds(List ids) throws InvalidApplicationException { + logger.debug(new MapLogEntry("collecting to delete").And("count", Optional.ofNullable(ids).map(List::size).orElse(0)).And("ids", ids)); + List data = this.queryFactory.query(TenantQuery.class).ids(ids).collect(); + logger.trace("retrieved {} items", Optional.ofNullable(data).map(List::size).orElse(0)); + this.deleteAndSave(data); + } + + public void deleteAndSave(List data) throws InvalidApplicationException { + logger.debug("will delete {} items", Optional.ofNullable(data).map(List::size).orElse(0)); + this.delete(data); + logger.trace("saving changes"); + this.entityManager.flush(); + logger.trace("changes saved"); + } + + public void delete(List data) throws InvalidApplicationException { + logger.debug("will delete {} items", Optional.ofNullable(data).map(List::size).orElse(0)); + if (data == null || data.isEmpty()) + return; + + Instant now = Instant.now(); + + for (TenantEntity item : data) { + logger.trace("deleting item {}", item.getId()); + item.setIsActive(IsActive.Inactive); + item.setUpdatedAt(now); + logger.trace("updating item"); + this.entityManager.merge(item); + logger.trace("updated item"); + } + } + +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/persist/TenantPersist.java b/dmp-backend/core/src/main/java/eu/eudat/model/persist/TenantPersist.java new file mode 100644 index 000000000..e90cd934d --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/persist/TenantPersist.java @@ -0,0 +1,80 @@ +package eu.eudat.model.persist; + +import eu.eudat.commons.validation.FieldNotNullIfOtherSet; +import eu.eudat.commons.validation.ValidId; +import jakarta.validation.constraints.*; + +import java.util.UUID; + +@FieldNotNullIfOtherSet(message = "{validation.hashempty}") +public class TenantPersist { + + @ValidId(message = "{validation.invalidid}") + private UUID id; + + @NotNull(message = "{validation.empty}") + @NotEmpty(message = "{validation.empty}") + @Size(max= 200, message = "{validation.largerthanmax}") + private String code; + + @NotNull(message = "{validation.empty}") + @NotEmpty(message = "{validation.empty}") + @Size(max= 500, message = "{validation.largerthanmax}") + private String name; + + @NotNull(message = "{validation.empty}") + @NotEmpty(message = "{validation.empty}") + private String description; + + private String config; + + private String hash; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getConfig() { + return config; + } + + public void setConfig(String config) { + this.config = config; + } + + public String getHash() { + return hash; + } + + public void setHash(String hash) { + this.hash = hash; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/query/TenantQuery.java b/dmp-backend/core/src/main/java/eu/eudat/query/TenantQuery.java new file mode 100644 index 000000000..90e81fbf7 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/query/TenantQuery.java @@ -0,0 +1,161 @@ +package eu.eudat.query; + + +import eu.eudat.authorization.AuthorizationFlags; +import eu.eudat.commons.enums.IsActive; +import eu.eudat.data.DmpBlueprintEntity; +import eu.eudat.data.TenantEntity; +import eu.eudat.model.Tenant; +import gr.cite.tools.data.query.FieldResolver; +import gr.cite.tools.data.query.QueryBase; +import gr.cite.tools.data.query.QueryContext; +import jakarta.persistence.Tuple; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.*; + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class TenantQuery extends QueryBase { + + private String like; + private Collection ids; + private Collection isActives; + private Collection excludedIds; + private EnumSet authorize = EnumSet.of(AuthorizationFlags.None); + + public TenantQuery like(String value) { + this.like = value; + return this; + } + + public TenantQuery ids(UUID value) { + this.ids = List.of(value); + return this; + } + + public TenantQuery ids(UUID... value) { + this.ids = Arrays.asList(value); + return this; + } + + public TenantQuery ids(Collection values) { + this.ids = values; + return this; + } + + public TenantQuery isActive(IsActive value) { + this.isActives = List.of(value); + return this; + } + + public TenantQuery isActive(IsActive... value) { + this.isActives = Arrays.asList(value); + return this; + } + + public TenantQuery isActive(Collection values) { + this.isActives = values; + return this; + } + + public TenantQuery excludedIds(Collection values) { + this.excludedIds = values; + return this; + } + + public TenantQuery excludedIds(UUID value) { + this.excludedIds = List.of(value); + return this; + } + + public TenantQuery excludedIds(UUID... value) { + this.excludedIds = Arrays.asList(value); + return this; + } + + public TenantQuery authorize(EnumSet values) { + this.authorize = values; + return this; + } + + @Override + protected Boolean isFalseQuery() { + return this.isEmpty(this.ids) || this.isEmpty(this.isActives); + } + + @Override + protected Class entityClass() { + return TenantEntity.class; + } + + @Override + protected Predicate applyFilters(QueryContext queryContext) { + List predicates = new ArrayList<>(); + if (this.ids != null) { + CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(TenantEntity._id)); + for (UUID item : this.ids) inClause.value(item); + predicates.add(inClause); + } + + if (this.like != null && !this.like.isEmpty()) { + predicates.add(queryContext.CriteriaBuilder.or(queryContext.CriteriaBuilder.like(queryContext.Root.get(TenantEntity._code), this.like), + queryContext.CriteriaBuilder.like(queryContext.Root.get(TenantEntity._name), this.like) + )); + } + + if (this.isActives != null) { + CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(TenantEntity._isActive)); + for (IsActive item : this.isActives) inClause.value(item); + predicates.add(inClause); + } + + if (this.excludedIds != null) { + CriteriaBuilder.In notInClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(TenantEntity._id)); + for (UUID item : this.excludedIds) + notInClause.value(item); + predicates.add(notInClause.not()); + } + + if (predicates.size() > 0) { + Predicate[] predicatesArray = predicates.toArray(new Predicate[0]); + return queryContext.CriteriaBuilder.and(predicatesArray); + } else { + return null; + } + + } + + @Override + protected TenantEntity convert(Tuple tuple, Set columns) { + TenantEntity item = new TenantEntity(); + item.setId(QueryBase.convertSafe(tuple, columns, TenantEntity._id, UUID.class)); + item.setCode(QueryBase.convertSafe(tuple, columns, TenantEntity._code, String.class)); + item.setName(QueryBase.convertSafe(tuple, columns, TenantEntity._name, String.class)); + item.setDescription(QueryBase.convertSafe(tuple, columns, TenantEntity._description, String.class)); + item.setConfig(QueryBase.convertSafe(tuple, columns, TenantEntity._config, String.class)); + item.setCreatedAt(QueryBase.convertSafe(tuple, columns, TenantEntity._createdAt, Instant.class)); + item.setUpdatedAt(QueryBase.convertSafe(tuple, columns, TenantEntity._updatedAt, Instant.class)); + item.setIsActive(QueryBase.convertSafe(tuple, columns, TenantEntity._isActive, IsActive.class)); + return item; + } + + @Override + protected String fieldNameOf(FieldResolver item) { + if (item.match(Tenant._id)) return TenantEntity._id; + else if (item.match(Tenant._code)) return TenantEntity._code; + else if (item.match(Tenant._name)) return TenantEntity._name; + else if (item.match(Tenant._description)) return TenantEntity._description; + else if (item.match(Tenant._config)) return TenantEntity._config; + else if (item.match(Tenant._createdAt)) return TenantEntity._createdAt; + else if (item.match(Tenant._updatedAt)) return TenantEntity._updatedAt; + else if (item.match(Tenant._isActive)) return TenantEntity._isActive; + else return null; + } + +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/query/lookup/TenantLookup.java b/dmp-backend/core/src/main/java/eu/eudat/query/lookup/TenantLookup.java new file mode 100644 index 000000000..599cef755 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/query/lookup/TenantLookup.java @@ -0,0 +1,61 @@ +package eu.eudat.query.lookup; + +import eu.eudat.commons.enums.IsActive; +import eu.eudat.query.TenantQuery; +import gr.cite.tools.data.query.Lookup; +import gr.cite.tools.data.query.QueryFactory; + +import java.util.List; +import java.util.UUID; + +public class TenantLookup extends Lookup { + + private String like; + private List isActive; + private List ids; + private List excludedIds; + + public String getLike() { + return like; + } + + public void setLike(String like) { + this.like = like; + } + + public List getIsActive() { + return isActive; + } + + public void setIsActive(List isActive) { + this.isActive = isActive; + } + + public List getIds() { + return ids; + } + + public void setIds(List ids) { + this.ids = ids; + } + + public List getExcludedIds() { + return excludedIds; + } + + public void setExcludedIds(List excludeIds) { + this.excludedIds = excludeIds; + } + + public TenantQuery enrich(QueryFactory queryFactory) { + TenantQuery query = queryFactory.query(TenantQuery.class); + if (this.like != null) query.like(this.like); + if (this.isActive != null) query.isActive(this.isActive); + if (this.ids != null) query.ids(this.ids); + if (this.excludedIds != null) query.excludedIds(this.excludedIds); + + this.enrichCommon(query); + + return query; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/service/tenant/TenantService.java b/dmp-backend/core/src/main/java/eu/eudat/service/tenant/TenantService.java new file mode 100644 index 000000000..c3a4650a0 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/service/tenant/TenantService.java @@ -0,0 +1,19 @@ +package eu.eudat.service.tenant; + +import eu.eudat.model.Tenant; +import eu.eudat.model.persist.TenantPersist; +import gr.cite.tools.exception.MyApplicationException; +import gr.cite.tools.exception.MyForbiddenException; +import gr.cite.tools.exception.MyNotFoundException; +import gr.cite.tools.exception.MyValidationException; +import gr.cite.tools.fieldset.FieldSet; + +import javax.management.InvalidApplicationException; +import java.util.UUID; + +public interface TenantService { + + Tenant persist(TenantPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException; + + void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException; +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/service/tenant/TenantServiceImpl.java b/dmp-backend/core/src/main/java/eu/eudat/service/tenant/TenantServiceImpl.java new file mode 100644 index 000000000..38ddfb31f --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/service/tenant/TenantServiceImpl.java @@ -0,0 +1,129 @@ +package eu.eudat.service.tenant; + +import eu.eudat.authorization.AuthorizationFlags; +import eu.eudat.authorization.Permission; +import eu.eudat.commons.XmlHandlingService; +import eu.eudat.commons.enums.IsActive; +import eu.eudat.convention.ConventionService; +import eu.eudat.data.TenantEntity; +import eu.eudat.errorcode.ErrorThesaurusProperties; +import eu.eudat.model.Tenant; +import eu.eudat.model.builder.TenantBuilder; +import eu.eudat.model.deleter.TenantDeleter; +import eu.eudat.model.persist.TenantPersist; +import eu.eudat.service.responseutils.ResponseUtilsService; +import gr.cite.commons.web.authz.service.AuthorizationService; +import gr.cite.tools.data.builder.BuilderFactory; +import gr.cite.tools.data.deleter.DeleterFactory; +import gr.cite.tools.data.query.QueryFactory; +import gr.cite.tools.exception.MyApplicationException; +import gr.cite.tools.exception.MyForbiddenException; +import gr.cite.tools.exception.MyNotFoundException; +import gr.cite.tools.exception.MyValidationException; +import gr.cite.tools.fieldset.BaseFieldSet; +import gr.cite.tools.fieldset.FieldSet; +import gr.cite.tools.logging.LoggerService; +import gr.cite.tools.logging.MapLogEntry; +import gr.cite.tools.validation.ValidationService; +import jakarta.persistence.EntityManager; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Service; + +import javax.management.InvalidApplicationException; +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +@Service +public class TenantServiceImpl implements TenantService { + + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(TenantServiceImpl.class)); + + private final EntityManager entityManager; + + private final AuthorizationService authorizationService; + + private final DeleterFactory deleterFactory; + + private final BuilderFactory builderFactory; + + private final ConventionService conventionService; + private final MessageSource messageSource; + private final QueryFactory queryFactory; + private final ResponseUtilsService responseUtilsService; + private final XmlHandlingService xmlHandlingService; + private final ErrorThesaurusProperties errors; + private final ValidationService validationService; + + @Autowired + public TenantServiceImpl( + EntityManager entityManager, + AuthorizationService authorizationService, + DeleterFactory deleterFactory, + BuilderFactory builderFactory, + ConventionService conventionService, + MessageSource messageSource, QueryFactory queryFactory, + ResponseUtilsService responseUtilsService, + XmlHandlingService xmlHandlingService, + ErrorThesaurusProperties errors, + ValidationService validationService) { + this.entityManager = entityManager; + this.authorizationService = authorizationService; + this.deleterFactory = deleterFactory; + this.builderFactory = builderFactory; + this.conventionService = conventionService; + this.messageSource = messageSource; + this.queryFactory = queryFactory; + this.responseUtilsService = responseUtilsService; + this.xmlHandlingService = xmlHandlingService; + this.errors = errors; + this.validationService = validationService; + } + + @Override + public Tenant persist(TenantPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException { + logger.debug(new MapLogEntry("persisting data").And("model", model).And("fields", fields)); + + this.authorizationService.authorizeForce(Permission.EditTenant); + + Boolean isUpdate = this.conventionService.isValidGuid(model.getId()); + + TenantEntity data; + if (isUpdate) { + data = this.entityManager.find(TenantEntity.class, model.getId()); + if (data == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{model.getId(), Tenant.class.getSimpleName()}, LocaleContextHolder.getLocale())); + if (!this.conventionService.hashValue(data.getUpdatedAt()).equals(model.getHash())) throw new MyValidationException(this.errors.getHashConflict().getCode(), this.errors.getHashConflict().getMessage()); + } else { + data = new TenantEntity(); + data.setId(UUID.randomUUID()); + data.setIsActive(IsActive.Active); + data.setCreatedAt(Instant.now()); + } + + data.setCode(model.getCode()); + data.setName(model.getName()); + data.setDescription(model.getDescription()); + data.setUpdatedAt(Instant.now()); + data.setConfig(model.getConfig()); + + if (isUpdate) this.entityManager.merge(data); + else this.entityManager.persist(data); + + this.entityManager.flush(); + + return this.builderFactory.builder(TenantBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).build(BaseFieldSet.build(fields, Tenant._id), data); + } + @Override + public void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException { + logger.debug("deleting : {}", id); + + this.authorizationService.authorizeForce(Permission.DeleteTenant); + + this.deleterFactory.deleter(TenantDeleter.class).deleteAndSaveByIds(List.of(id)); + } + +} + diff --git a/dmp-backend/web/src/main/java/eu/eudat/controllers/v2/TenantController.java b/dmp-backend/web/src/main/java/eu/eudat/controllers/v2/TenantController.java new file mode 100644 index 000000000..af16dfb60 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/controllers/v2/TenantController.java @@ -0,0 +1,135 @@ +package eu.eudat.controllers.v2; + +import com.fasterxml.jackson.core.JsonProcessingException; +import eu.eudat.audit.AuditableAction; +import eu.eudat.authorization.AuthorizationFlags; +import eu.eudat.data.TenantEntity; +import eu.eudat.model.Tenant; +import eu.eudat.model.builder.TenantBuilder; +import eu.eudat.model.censorship.TenantCensor; +import eu.eudat.model.persist.TenantPersist; +import eu.eudat.model.result.QueryResult; +import eu.eudat.query.TenantQuery; +import eu.eudat.query.lookup.TenantLookup; +import eu.eudat.service.tenant.TenantService; +import gr.cite.tools.auditing.AuditService; +import gr.cite.tools.data.builder.BuilderFactory; +import gr.cite.tools.data.censor.CensorFactory; +import gr.cite.tools.data.query.QueryFactory; +import gr.cite.tools.exception.MyApplicationException; +import gr.cite.tools.exception.MyForbiddenException; +import gr.cite.tools.exception.MyNotFoundException; +import gr.cite.tools.fieldset.FieldSet; +import gr.cite.tools.logging.LoggerService; +import gr.cite.tools.logging.MapLogEntry; +import gr.cite.tools.validation.MyValidate; +import jakarta.transaction.Transactional; +import jakarta.xml.bind.JAXBException; +import org.slf4j.LoggerFactory; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.web.bind.annotation.*; + +import javax.management.InvalidApplicationException; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; +import java.util.AbstractMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@RestController +@RequestMapping(path = "api/tenant") +public class TenantController { + + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(TenantController.class)); + + private final BuilderFactory builderFactory; + + private final AuditService auditService; + + private final TenantService tenantService; + + private final CensorFactory censorFactory; + + private final QueryFactory queryFactory; + + private final MessageSource messageSource; + + public TenantController( + BuilderFactory builderFactory, + AuditService auditService, + TenantService tenantService, + CensorFactory censorFactory, + QueryFactory queryFactory, + MessageSource messageSource) { + this.builderFactory = builderFactory; + this.auditService = auditService; + this.tenantService = tenantService; + this.censorFactory = censorFactory; + this.queryFactory = queryFactory; + this.messageSource = messageSource; + } + + @PostMapping("query") + public QueryResult query(@RequestBody TenantLookup lookup) throws MyApplicationException, MyForbiddenException { + logger.debug("querying {}", Tenant.class.getSimpleName()); + + this.censorFactory.censor(TenantCensor.class).censor(lookup.getProject(), null); + TenantQuery query = lookup.enrich(this.queryFactory).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic); + + List data = query.collectAs(lookup.getProject()); + List models = this.builderFactory.builder(TenantBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).build(lookup.getProject(), data); + long count = (lookup.getMetadata() != null && lookup.getMetadata().getCountAll()) ? query.count() : models.size(); + + this.auditService.track(AuditableAction.Tenant_Query, "lookup", lookup); + + return new QueryResult<>(models, count); + } + + @GetMapping("{id}") + public Tenant get(@PathVariable("id") UUID id, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException { + logger.debug(new MapLogEntry("retrieving" + Tenant.class.getSimpleName()).And("id", id).And("fields", fieldSet)); + + this.censorFactory.censor(TenantCensor.class).censor(fieldSet, null); + + TenantQuery query = this.queryFactory.query(TenantQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).ids(id); + Tenant model = this.builderFactory.builder(TenantBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).build(fieldSet, query.firstAs(fieldSet)); + if (model == null) + throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{id, Tenant.class.getSimpleName()}, LocaleContextHolder.getLocale())); + + this.auditService.track(AuditableAction.Tenant_Lookup, Map.ofEntries( + new AbstractMap.SimpleEntry("id", id), + new AbstractMap.SimpleEntry("fields", fieldSet) + )); + + return model; + } + + @PostMapping("persist") + @Transactional + public Tenant persist(@MyValidate @RequestBody TenantPersist model, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException, JAXBException, ParserConfigurationException, JsonProcessingException, TransformerException { + logger.debug(new MapLogEntry("persisting" + Tenant.class.getSimpleName()).And("model", model).And("fieldSet", fieldSet)); + this.censorFactory.censor(TenantCensor.class).censor(fieldSet, null); + + Tenant persisted = this.tenantService.persist(model, fieldSet); + + this.auditService.track(AuditableAction.Tenant_Persist, Map.ofEntries( + new AbstractMap.SimpleEntry("model", model), + new AbstractMap.SimpleEntry("fields", fieldSet) + )); + + return persisted; + } + + @DeleteMapping("{id}") + @Transactional + public void delete(@PathVariable("id") UUID id) throws MyForbiddenException, InvalidApplicationException { + logger.debug(new MapLogEntry("retrieving" + Tenant.class.getSimpleName()).And("id", id)); + + this.tenantService.deleteAndSave(id); + + this.auditService.track(AuditableAction.Tenant_Delete, "id", id); + } + +} diff --git a/dmp-backend/web/src/main/resources/config/permissions.yml b/dmp-backend/web/src/main/resources/config/permissions.yml index 259aaf5fd..442863af6 100644 --- a/dmp-backend/web/src/main/resources/config/permissions.yml +++ b/dmp-backend/web/src/main/resources/config/permissions.yml @@ -490,6 +490,27 @@ permissions: allowAnonymous: false allowAuthenticated: false + # ReferenceType Permissions + BrowseTenant: + roles: + - Admin + clients: [ ] + allowAnonymous: false + allowAuthenticated: false + EditTenant: + roles: + - Admin + clients: [ ] + allowAnonymous: false + allowAuthenticated: false + DeleteTenant: + roles: + - Admin + claims: [ ] + clients: [ ] + allowAnonymous: false + allowAuthenticated: false + # DmpDescriptionTemplate Permissions BrowseDmpDescriptionTemplate: diff --git a/dmp-frontend/src/app/app-routing.module.ts b/dmp-frontend/src/app/app-routing.module.ts index be98afa37..beeb94565 100644 --- a/dmp-frontend/src/app/app-routing.module.ts +++ b/dmp-frontend/src/app/app-routing.module.ts @@ -252,7 +252,15 @@ const appRoutes: Routes = [ loadChildren: () => import('./ui/admin/reference-type/reference-type.module').then(m => m.ReferenceTypeModule), data: { breadcrumb: true, - title: 'GENERAL.TITLES.REFERENCE-TYPE' + title: 'GENERAL.TITLES.REFERENCE-TYPES' + }, + }, + { + path: 'tenants', + loadChildren: () => import('./ui/admin/tenant/tenant.module').then(m => m.TenantModule), + data: { + breadcrumb: true, + title: 'GENERAL.TITLES.TENANTS' }, }, { 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 eace39451..6918b587c 100644 --- a/dmp-frontend/src/app/core/common/enum/permission.enum.ts +++ b/dmp-frontend/src/app/core/common/enum/permission.enum.ts @@ -23,5 +23,10 @@ export enum AppPermission { BrowseReferenceType = "BrowseReferenceType", EditReferenceType = "EditReferenceType", DeleteReferenceType = "DeleteReferenceType", + + //Tenant + BrowseTenant = "BrowseTenant", + EditTenant = "EditTenant", + DeleteTenant = "DeleteTenant", } diff --git a/dmp-frontend/src/app/core/core-service.module.ts b/dmp-frontend/src/app/core/core-service.module.ts index 902343733..a35754c57 100644 --- a/dmp-frontend/src/app/core/core-service.module.ts +++ b/dmp-frontend/src/app/core/core-service.module.ts @@ -59,6 +59,7 @@ import { FileUtils } from './services/utilities/file-utils.service'; import { QueryParamsService } from './services/utilities/query-params.service'; import { DescriptionTemplateService } from './services/description-template/description-template.service'; import { ReferenceTypeService } from './services/reference-type/reference-type.service'; +import { TenantService } from './services/tenant/tenant.service'; // // // This is shared module that provides all the services. Its imported only once on the AppModule. @@ -136,7 +137,8 @@ export class CoreServiceModule { FileUtils, ReferenceService, DescriptionTemplateService, - ReferenceTypeService + ReferenceTypeService, + TenantService ], }; } diff --git a/dmp-frontend/src/app/core/model/tenant/tenant.ts b/dmp-frontend/src/app/core/model/tenant/tenant.ts new file mode 100644 index 000000000..6a655b5bf --- /dev/null +++ b/dmp-frontend/src/app/core/model/tenant/tenant.ts @@ -0,0 +1,17 @@ +import { BaseEntity, BaseEntityPersist } from "@common/base/base-entity.model"; + +export interface Tenant extends BaseEntity{ + name: string; + code: string; + description: string; + config: string; +} + +//persist + +export interface TenantPersist extends BaseEntityPersist{ + name: string; + code: string; + description: string; + config: string; +} \ No newline at end of file diff --git a/dmp-frontend/src/app/core/query/tenant.lookup.ts b/dmp-frontend/src/app/core/query/tenant.lookup.ts new file mode 100644 index 000000000..9a3611341 --- /dev/null +++ b/dmp-frontend/src/app/core/query/tenant.lookup.ts @@ -0,0 +1,21 @@ +import { Lookup } from '@common/model/lookup'; +import { Guid } from '@common/types/guid'; +import { IsActive } from '../common/enum/is-active.enum'; + +export class TenantLookup extends Lookup implements TenantFilter { + ids: Guid[]; + excludedIds: Guid[]; + like: string; + isActive: IsActive[]; + + constructor() { + super(); + } +} + +export interface TenantFilter { + ids: Guid[]; + excludedIds: Guid[]; + like: string; + isActive: IsActive[]; +} \ No newline at end of file diff --git a/dmp-frontend/src/app/core/services/tenant/tenant.service.ts b/dmp-frontend/src/app/core/services/tenant/tenant.service.ts new file mode 100644 index 000000000..c498d5de2 --- /dev/null +++ b/dmp-frontend/src/app/core/services/tenant/tenant.service.ts @@ -0,0 +1,94 @@ +import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { IsActive } from '@app/core/common/enum/is-active.enum'; +import { Tenant, TenantPersist } from '@app/core/model/tenant/tenant'; +import { TenantLookup } from '@app/core/query/tenant.lookup'; +import { MultipleAutoCompleteConfiguration } from '@app/library/auto-complete/multiple/multiple-auto-complete-configuration'; +import { SingleAutoCompleteConfiguration } from '@app/library/auto-complete/single/single-auto-complete-configuration'; +import { QueryResult } from '@common/model/query-result'; +import { FilterService } from '@common/modules/text-filter/filter-service'; +import { Guid } from '@common/types/guid'; +import { Observable, throwError } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { nameof } from 'ts-simple-nameof'; +import { ConfigurationService } from '../configuration/configuration.service'; +import { BaseHttpV2Service } from '../http/base-http-v2.service'; + +@Injectable() +export class TenantService { + + constructor(private http: BaseHttpV2Service, private configurationService: ConfigurationService, private filterService: FilterService) { + } + + private get apiBase(): string { return `${this.configurationService.server}tenant`; } + + query(q: TenantLookup): Observable> { + const url = `${this.apiBase}/query`; + return this.http.post>(url, q).pipe(catchError((error: any) => throwError(error))); + } + + getSingle(id: Guid, reqFields: string[] = []): Observable { + const url = `${this.apiBase}/${id}`; + const options = { params: { f: reqFields } }; + + return this.http + .get(url, options).pipe( + catchError((error: any) => throwError(error))); + } + + persist(item: TenantPersist): Observable { + const url = `${this.apiBase}/persist`; + + return this.http + .post(url, item).pipe( + catchError((error: any) => throwError(error))); + } + + delete(id: Guid): Observable { + const url = `${this.apiBase}/${id}`; + + return this.http + .delete(url).pipe( + catchError((error: any) => throwError(error))); + } + + // + // Autocomplete Commons + // + // tslint:disable-next-line: member-ordering + singleAutocompleteConfiguration: SingleAutoCompleteConfiguration = { + initialItems: (data?: any) => this.query(this.buildAutocompleteLookup()).pipe(map(x => x.items)), + filterFn: (searchQuery: string, data?: any) => this.query(this.buildAutocompleteLookup(searchQuery)).pipe(map(x => x.items)), + getSelectedItem: (selectedItem: any) => this.query(this.buildAutocompleteLookup(null, null, [selectedItem])).pipe(map(x => x.items[0])), + displayFn: (item: Tenant) => item.name, + titleFn: (item: Tenant) => item.name, + valueAssign: (item: Tenant) => item.id, + }; + + // tslint:disable-next-line: member-ordering + multipleAutocompleteConfiguration: MultipleAutoCompleteConfiguration = { + initialItems: (excludedItems: any[], data?: any) => this.query(this.buildAutocompleteLookup(null, excludedItems ? excludedItems : null)).pipe(map(x => x.items)), + filterFn: (searchQuery: string, excludedItems: any[]) => this.query(this.buildAutocompleteLookup(searchQuery, excludedItems)).pipe(map(x => x.items)), + getSelectedItems: (selectedItems: any[]) => this.query(this.buildAutocompleteLookup(null, null, selectedItems)).pipe(map(x => x.items)), + displayFn: (item: Tenant) => item.name, + titleFn: (item: Tenant) => item.name, + valueAssign: (item: Tenant) => item.id, + }; + + private buildAutocompleteLookup(like?: string, excludedIds?: Guid[], ids?: Guid[]): TenantLookup { + const lookup: TenantLookup = new TenantLookup(); + lookup.page = { size: 100, offset: 0 }; + if (excludedIds && excludedIds.length > 0) { lookup.excludedIds = excludedIds; } + if (ids && ids.length > 0) { lookup.ids = ids; } + lookup.isActive = [IsActive.Active]; + lookup.project = { + fields: [ + nameof(x => x.id), + nameof(x => x.name) + ] + }; + lookup.order = { items: [nameof(x => x.name)] }; + if (like) { lookup.like = this.filterService.transformLike(like); } + return lookup; + } +} \ No newline at end of file diff --git a/dmp-frontend/src/app/ui/admin/reference-type/editor/reference-type-editor.resolver.ts b/dmp-frontend/src/app/ui/admin/reference-type/editor/reference-type-editor.resolver.ts index eb5f16855..b04b55898 100644 --- a/dmp-frontend/src/app/ui/admin/reference-type/editor/reference-type-editor.resolver.ts +++ b/dmp-frontend/src/app/ui/admin/reference-type/editor/reference-type-editor.resolver.ts @@ -80,12 +80,8 @@ export class ReferenceTypeEditorResolver extends BaseEditorResolver { ...ReferenceTypeEditorResolver.lookupFields() ]; const id = route.paramMap.get('id'); - //const cloneid = route.paramMap.get('cloneid'); if (id != null) { return this.ReferenceTypeService.getSingle(Guid.parse(id), fields).pipe(tap(x => this.breadcrumbService.addIdResolvedValue(x.id?.toString(), x.code)), takeUntil(this._destroyed)); } - // } else if (cloneid != null) { - // return this.ReferenceTypeService.clone(Guid.parse(cloneid), fields).pipe(tap(x => this.breadcrumbService.addIdResolvedValue(x.id?.toString(), x.label)), takeUntil(this._destroyed)); - // } } } diff --git a/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.component.html b/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.component.html new file mode 100644 index 000000000..6726eb2ed --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.component.html @@ -0,0 +1,71 @@ +
+
+
+

{{'TENANT-EDITOR.NEW' | translate}}

+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + + {{'TENANT-EDITOR.NEW' | translate}} + + +
+
+ + {{'TENANT-EDITOR.FIELDS.NAME' | translate}} + + + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + +
+
+ + {{'TENANT-EDITOR.FIELDS.CODE' | translate}} + + + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + +
+
+

{{'TENANT-EDITOR.FIELDS.DESCRIPTION' | translate}}

+
+ + +
+ + {{'GENERAL.VALIDATION.REQUIRED'| translate}} + +
+
+
+
+ + {{'TENANT-EDITOR.FIELDS.NAME' | translate}} + + + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + +
+
+
+
+
+
\ No newline at end of file diff --git a/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.component.scss b/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.component.scss new file mode 100644 index 000000000..960ac2616 --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.component.scss @@ -0,0 +1,116 @@ +.tenant-editor { + margin-top: 1.3rem; + margin-left: 1em; + margin-right: 3em; + + .remove { + background-color: white; + color: black; + } + + .add { + background-color: white; + color: #009700; + } +} + +::ng-deep .mat-checkbox-checked.mat-accent .mat-checkbox-background, .mat-checkbox-indeterminate.mat-accent .mat-checkbox-background { + background-color: var(--primary-color-3); + // background-color: #0070c0; +} + +::ng-deep .mat-checkbox-disabled.mat-checkbox-checked .mat-checkbox-background, .mat-checkbox-disabled.mat-checkbox-indeterminate .mat-checkbox-background { + background-color: #b0b0b0; +} + +.finalize-btn { + border-radius: 30px; + border: 1px solid var(--primary-color); + background: transparent; + padding-left: 2em; + padding-right: 2em; + box-shadow: 0px 3px 6px #1E202029; + color: var(--primary-color); + &:disabled{ + background-color: #CBCBCB; + color: #FFF; + border: 0px; + } +} + +.action-btn { + border-radius: 30px; + background-color: var(--secondary-color); + border: 1px solid transparent; + padding-left: 2em; + padding-right: 2em; + box-shadow: 0px 3px 6px #1E202029; + + transition-property: background-color, color; + transition-duration: 200ms; + transition-delay: 50ms; + transition-timing-function: ease-in-out; + &:disabled{ + background-color: #CBCBCB; + color: #FFF; + border: 0px; + } +} + +.dlt-section-btn { + margin: 0; + position: absolute; + top: 50%; + -ms-transform: translateY(-50%); + transform: translateY(-50%); +} + +.section-input { + position: relative; +} + +.section-input .arrows { + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); +} + +.action-list-item{ + display: flex; + align-items: center; + cursor: pointer; + + .action-list-icon{ + font-size: 1.2em; + // padding-right: 1em; + // width: 14px; + // margin-right: 0.5em; + // margin-left: -.09em; + // height: auto; + color: var(--primary-color); + } + + .action-list-text{ + font-size: 1em; + color: var(--primary-color); + } +} + +.field-delete{ + align-items: center; + display: flex; + cursor: pointer; + + .field-delete-icon{ + font-size: 1.2em; + width: 14px; + color: var(--primary-color); + } + + .field-delete-text{ + font-size: 1em; + margin-left: 0.5em; + color: var(--primary-color); + } +} \ No newline at end of file diff --git a/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.component.ts b/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.component.ts new file mode 100644 index 000000000..4ce5112ee --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.component.ts @@ -0,0 +1,185 @@ + +import { Component, OnInit } from '@angular/core'; +import { FormArray, UntypedFormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TenantService } from '@app/core/services/tenant/tenant.service'; +import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; +import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; +// import { BreadcrumbItem } from '@app/ui/misc/breadcrumb/definition/breadcrumb-item'; +import { DatePipe } from '@angular/common'; +import { IsActive } from '@app/core/common/enum/is-active.enum'; +import { AppPermission } from '@app/core/common/enum/permission.enum'; +import { DatasetProfileModel } from '@app/core/model/dataset/dataset-profile'; +import { Tenant, TenantPersist } from '@app/core/model/tenant/tenant'; +import { AuthService } from '@app/core/services/auth/auth.service'; +import { DmpService } from '@app/core/services/dmp/dmp.service'; +import { LoggingService } from '@app/core/services/logging/logging-service'; +import { MatomoService } from '@app/core/services/matomo/matomo-service'; +import { FileUtils } from '@app/core/services/utilities/file-utils.service'; +import { QueryParamsService } from '@app/core/services/utilities/query-params.service'; +import { MultipleAutoCompleteConfiguration } from '@app/library/auto-complete/multiple/multiple-auto-complete-configuration'; +import { BaseEditor } from '@common/base/base-editor'; +import { FormService } from '@common/forms/form-service'; +import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component'; +import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service'; +import { FilterService } from '@common/modules/text-filter/filter-service'; +import { Guid } from '@common/types/guid'; +import { TranslateService } from '@ngx-translate/core'; +import { map, takeUntil } from 'rxjs/operators'; +import { TenantEditorResolver } from './tenant-editor.resolver'; +import { TenantEditorService } from './tenant-editor.service'; +import { TenantEditorModel } from './tenant-editor.model'; + + +@Component({ + selector: 'app-tenant-editor-component', + templateUrl: 'tenant-editor.component.html', + styleUrls: ['./tenant-editor.component.scss'], + providers: [TenantEditorService] +}) +export class TenantEditorComponent extends BaseEditor implements OnInit { + + isNew = true; + isDeleted = false; + formGroup: UntypedFormGroup = null; + showInactiveDetails = false; + + protected get canDelete(): boolean { + return !this.isDeleted && !this.isNew && this.hasPermission(this.authService.permissionEnum.DeleteTenant); + } + + protected get canSave(): boolean { + return !this.isDeleted && this.hasPermission(this.authService.permissionEnum.EditTenant); + } + + protected get canFinalize(): boolean { + return !this.isDeleted && this.hasPermission(this.authService.permissionEnum.EditTenant); + } + + + private hasPermission(permission: AppPermission): boolean { + return this.authService.hasPermission(permission) || this.editorModel?.permissions?.includes(permission); + } + + constructor( + // BaseFormEditor injected dependencies + protected dialog: MatDialog, + protected language: TranslateService, + protected formService: FormService, + protected router: Router, + protected uiNotificationService: UiNotificationService, + protected httpErrorHandlingService: HttpErrorHandlingService, + protected filterService: FilterService, + protected datePipe: DatePipe, + protected route: ActivatedRoute, + protected queryParamsService: QueryParamsService, + // Rest dependencies. Inject any other needed deps here: + public authService: AuthService, + public enumUtils: EnumUtils, + private tenantService: TenantService, + private logger: LoggingService, + private tenantEditorService: TenantEditorService, + private fileUtils: FileUtils, + private matomoService: MatomoService + ) { + super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, datePipe, route, queryParamsService); + } + + ngOnInit(): void { + this.matomoService.trackPageView('Admin: Tenants'); + super.ngOnInit(); + } + + getItem(itemId: Guid, successFunction: (item: Tenant) => void) { + this.tenantService.getSingle(itemId, TenantEditorResolver.lookupFields()) + .pipe(map(data => data as Tenant), takeUntil(this._destroyed)) + .subscribe( + data => successFunction(data), + error => this.onCallbackError(error) + ); + } + + prepareForm(data: Tenant) { + try { + this.editorModel = data ? new TenantEditorModel().fromModel(data) : new TenantEditorModel(); + this.isDeleted = data ? data.isActive === IsActive.Inactive : false; + this.buildForm(); + } catch (error) { + this.logger.error('Could not parse Tenant item: ' + data + error); + this.uiNotificationService.snackBarNotification(this.language.instant('COMMONS.ERRORS.DEFAULT'), SnackBarNotificationLevel.Error); + } + } + + buildForm() { + this.formGroup = this.editorModel.buildForm(null, this.isDeleted || !this.authService.hasPermission(AppPermission.EditTenant)); + this.tenantEditorService.setValidationErrorModel(this.editorModel.validationErrorModel); + } + + refreshData(): void { + this.getItem(this.editorModel.id, (data: Tenant) => this.prepareForm(data)); + } + + refreshOnNavigateToData(id?: Guid): void { + this.formGroup.markAsPristine(); + let route = []; + + if (id === null) { + route.push('../..'); + } else if (this.isNew) { + route.push('../' + id); + } else { + route.push('..'); + } + + this.router.navigate(route, { queryParams: { 'lookup': this.queryParamsService.serializeLookup(this.lookupParams), 'lv': ++this.lv }, replaceUrl: true, relativeTo: this.route }); + } + + persistEntity(onSuccess?: (response) => void): void { + const formData = this.formService.getValue(this.formGroup.value) as TenantPersist; + + this.tenantService.persist(formData) + .pipe(takeUntil(this._destroyed)).subscribe( + complete => onSuccess ? onSuccess(complete) : this.onCallbackSuccess(complete), + error => this.onCallbackError(error) + ); + } + + formSubmit(): void { + this.formService.touchAllFormFields(this.formGroup); + if (!this.isFormValid()) { + return; + } + + this.persistEntity(); + } + + public delete() { + const value = this.formGroup.value; + if (value.id) { + const dialogRef = this.dialog.open(ConfirmationDialogComponent, { + maxWidth: '300px', + data: { + 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.tenantService.delete(value.id).pipe(takeUntil(this._destroyed)) + .subscribe( + complete => this.onCallbackSuccess(), + error => this.onCallbackError(error) + ); + } + }); + } + } + + clearErrorModel() { + this.editorModel.validationErrorModel.clear(); + this.formService.validateAllFormFields(this.formGroup); + } + +} diff --git a/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.model.ts b/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.model.ts new file mode 100644 index 000000000..d8e667b8b --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.model.ts @@ -0,0 +1,58 @@ +import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms"; +import { Tenant, TenantPersist } from "@app/core/model/tenant/tenant"; +import { BaseEditorModel } from "@common/base/base-form-editor-model"; +import { BackendErrorValidator } from "@common/forms/validation/custom-validator"; +import { ValidationErrorModel } from "@common/forms/validation/error-model/validation-error-model"; +import { Validation, ValidationContext } from "@common/forms/validation/validation-context"; + +export class TenantEditorModel extends BaseEditorModel implements TenantPersist { + name: string; + code: string; + description: string; + config: string; + permissions: string[]; + + public validationErrorModel: ValidationErrorModel = new ValidationErrorModel(); + protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder(); + + constructor() { super(); } + + public fromModel(item: Tenant): TenantEditorModel { + if (item) { + super.fromModel(item); + this.name = item.name; + this.code = item.code; + this.description = item.description; + this.config = item.config; + } + return this; + } + + buildForm(context: ValidationContext = null, disabled: boolean = false): UntypedFormGroup { + if (context == null) { context = this.createValidationContext(); } + + return this.formBuilder.group({ + id: [{ value: this.id, disabled: disabled }, context.getValidation('id').validators], + name: [{ value: this.name, disabled: disabled }, context.getValidation('name').validators], + code: [{ value: this.code, disabled: disabled }, context.getValidation('code').validators], + description: [{ value: this.description, disabled: disabled }, context.getValidation('description').validators], + config: [{ value: this.config, disabled: disabled }, context.getValidation('config').validators], + hash: [{ value: this.hash, disabled: disabled }, context.getValidation('hash').validators] + }); + } + + createValidationContext(): ValidationContext { + const baseContext: ValidationContext = new ValidationContext(); + const baseValidationArray: Validation[] = new Array(); + baseValidationArray.push({ key: 'id', validators: [BackendErrorValidator(this.validationErrorModel, 'id')] }); + baseValidationArray.push({ key: 'name', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'name')] }); + baseValidationArray.push({ key: 'code', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'code')] }); + baseValidationArray.push({ key: 'description', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'description')] }); + baseValidationArray.push({ key: 'config', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'config')] }); + baseValidationArray.push({ key: 'hash', validators: [] }); + + baseContext.validation = baseValidationArray; + return baseContext; + } +} + diff --git a/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.resolver.ts b/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.resolver.ts new file mode 100644 index 000000000..0eb0d764e --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.resolver.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { Tenant } from '@app/core/model/tenant/tenant'; +import { TenantService } from '@app/core/services/tenant/tenant.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 { takeUntil, tap } from 'rxjs/operators'; +import { nameof } from 'ts-simple-nameof'; + +@Injectable() +export class TenantEditorResolver extends BaseEditorResolver { + + constructor(private TenantService: TenantService, private breadcrumbService: BreadcrumbService) { + super(); + } + + public static lookupFields(): string[] { + return [ + ...BaseEditorResolver.lookupFields(), + nameof(x => x.id), + nameof(x => x.name), + nameof(x => x.code), + nameof(x => x.description), + nameof(x => x.config), + nameof(x => x.createdAt), + nameof(x => x.updatedAt), + nameof(x => x.hash), + nameof(x => x.isActive) + ] + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + + const fields = [ + ...TenantEditorResolver.lookupFields() + ]; + const id = route.paramMap.get('id'); + + if (id != null) { + return this.TenantService.getSingle(Guid.parse(id), fields).pipe(tap(x => this.breadcrumbService.addIdResolvedValue(x.id?.toString(), x.code)), takeUntil(this._destroyed)); + } + } +} diff --git a/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.service.ts b/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.service.ts new file mode 100644 index 000000000..8f53e9bbe --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/tenant/editor/tenant-editor.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from "@angular/core"; +import { ValidationErrorModel } from "@common/forms/validation/error-model/validation-error-model"; + +@Injectable() +export class TenantEditorService { + private validationErrorModel: ValidationErrorModel; + + public setValidationErrorModel(validationErrorModel: ValidationErrorModel): void { + this.validationErrorModel = validationErrorModel; + } + + public getValidationErrorModel(): ValidationErrorModel { + return this.validationErrorModel; + } +} diff --git a/dmp-frontend/src/app/ui/admin/tenant/listing/filters/tenant-listing-filters.component.html b/dmp-frontend/src/app/ui/admin/tenant/listing/filters/tenant-listing-filters.component.html new file mode 100644 index 000000000..248db4c57 --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/tenant/listing/filters/tenant-listing-filters.component.html @@ -0,0 +1,36 @@ +
+ + + + + +
+
+
+

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

+ +
+ + + {{'TENANT-LISTING.FILTER.IS-ACTIVE' | translate}} + + +
+ + +
+
+
+
+ + +
diff --git a/dmp-frontend/src/app/ui/admin/tenant/listing/filters/tenant-listing-filters.component.scss b/dmp-frontend/src/app/ui/admin/tenant/listing/filters/tenant-listing-filters.component.scss new file mode 100644 index 000000000..999f5a7c6 --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/tenant/listing/filters/tenant-listing-filters.component.scss @@ -0,0 +1,25 @@ +.description-template-type-listing-filters { + +} + +::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/tenant/listing/filters/tenant-listing-filters.component.ts b/dmp-frontend/src/app/ui/admin/tenant/listing/filters/tenant-listing-filters.component.ts new file mode 100644 index 000000000..c895267ac --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/tenant/listing/filters/tenant-listing-filters.component.ts @@ -0,0 +1,94 @@ +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { IsActive } from '@app/core/common/enum/is-active.enum'; +import { TenantFilter } from '@app/core/query/tenant.lookup'; +import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; +import { BaseComponent } from '@common/base/base.component'; +import { nameof } from 'ts-simple-nameof'; + +@Component({ + selector: 'app-tenant-listing-filters', + templateUrl: './tenant-listing-filters.component.html', + styleUrls: ['./tenant-listing-filters.component.scss'] +}) +export class TenantListingFiltersComponent extends BaseComponent implements OnInit, OnChanges { + + @Input() readonly filter: TenantFilter; + @Output() filterChange = new EventEmitter(); + + // * State + internalFilters: TenantListingFilters = this._getEmptyFilters(); + + protected appliedFilterCount: number = 0; + constructor( + public enumUtils: EnumUtils, + ) { super(); } + + ngOnInit() { + } + + ngOnChanges(changes: SimpleChanges): void { + const filterChange = changes[nameof(x => x.filter)]?.currentValue as TenantFilter; + 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 { isActive, like } = this.internalFilters ?? {} + this.filterChange.emit({ + ...this.filter, + like, + isActive: isActive ? [IsActive.Active] : [IsActive.Inactive] + }) + } + + + private _parseToInternalFilters(inputFilter: TenantFilter): TenantListingFilters { + if (!inputFilter) { + return this._getEmptyFilters(); + } + + let { excludedIds, ids, isActive, like } = inputFilter; + + return { + isActive: (isActive ?? [])?.includes(IsActive.Active) || !isActive?.length, + like: like + } + + } + + private _getEmptyFilters(): TenantListingFilters { + return { + isActive: true, + like: null, + } + } + + private _computeAppliedFilters(filters: TenantListingFilters): number { + let count = 0; + if (filters?.isActive) { + count++ + } + return count; + } + + clearFilters() { + this.internalFilters = this._getEmptyFilters(); + } +} + +interface TenantListingFilters { + isActive: boolean; + like: string; +} diff --git a/dmp-frontend/src/app/ui/admin/tenant/listing/tenant-listing.component.html b/dmp-frontend/src/app/ui/admin/tenant/listing/tenant-listing.component.html new file mode 100644 index 000000000..827f5421f --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/tenant/listing/tenant-listing.component.html @@ -0,0 +1,108 @@ +
+
+ +
+
+

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

+ + +
+
+ +
+
+ + + + + + + + + +
+
+ + + + +
+
+ + {{item?.name | nullifyValue}} +
+
+ + +
+
+ {{enumUtils.toDescriptionTemplateTypeStatusString(item.status) | nullifyValue}} +
+
+
+ + + + {{'TENANT-LISTING.FIELDS.CREATED-AT' | translate}}: + + {{item?.createdAt | dateTimeFormatter : 'short' | nullifyValue}} + + +
+
+ + + {{'TENANT-LISTING.FIELDS.UPDATED-AT' | translate}}: + + {{item?.updatedAt | dateTimeFormatter : 'short' | nullifyValue}} + + + +
+
+
+ + + + +
+
+ + + + + + +
+
+
\ No newline at end of file diff --git a/dmp-frontend/src/app/ui/admin/tenant/listing/tenant-listing.component.scss b/dmp-frontend/src/app/ui/admin/tenant/listing/tenant-listing.component.scss new file mode 100644 index 000000000..6e1b48814 --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/tenant/listing/tenant-listing.component.scss @@ -0,0 +1,60 @@ +.description-template-type-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; + } +} +.create-btn { + border-radius: 30px; + background-color: var(--secondary-color); + padding-left: 2em; + padding-right: 2em; + // color: #000; + + .button-text{ + display: inline-block; + } +} + +.dlt-btn { + color: rgba(0, 0, 0, 0.54); +} + +.status-chip{ + + border-radius: 20px; + padding-left: 1em; + padding-right: 1em; + padding-top: 0.2em; + font-size: .8em; +} + +.status-chip-finalized{ + color: #568b5a; + background: #9dd1a1 0% 0% no-repeat padding-box; +} + +.status-chip-draft{ + color: #00c4ff; + background: #d3f5ff 0% 0% no-repeat padding-box; +} diff --git a/dmp-frontend/src/app/ui/admin/tenant/listing/tenant-listing.component.ts b/dmp-frontend/src/app/ui/admin/tenant/listing/tenant-listing.component.ts new file mode 100644 index 000000000..ed1007250 --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/tenant/listing/tenant-listing.component.ts @@ -0,0 +1,170 @@ +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { IsActive } from '@app/core/common/enum/is-active.enum'; +import { Tenant } from '@app/core/model/tenant/tenant'; +import { TenantLookup } from '@app/core/query/tenant.lookup'; +import { AuthService } from '@app/core/services/auth/auth.service'; +import { TenantService } from '@app/core/services/tenant/tenant.service'; +import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-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'; + +@Component({ + templateUrl: './tenant-listing.component.html', + styleUrls: ['./tenant-listing.component.scss'] +}) +export class TenantListingComponent extends BaseListingComponent implements OnInit { + publish = false; + userSettingsKey = { key: 'TenantListingUserSettings' }; + propertiesAvailableForOrder: ColumnDefinition[]; + + // @ViewChild('TenantStatus', { static: true }) TenantStatus?: TemplateRef; + @ViewChild('actions', { static: true }) actions?: TemplateRef; + @ViewChild(HybridListingComponent, { static: true }) hybridListingComponent: HybridListingComponent; + + private readonly lookupFields: string[] = [ + nameof(x => x.id), + nameof(x => x.name), + nameof(x => x.code), + nameof(x => x.updatedAt), + nameof(x => x.createdAt), + nameof(x => x.hash), + nameof(x => x.isActive) + ]; + + rowIdentity = x => x.id; + + constructor( + protected router: Router, + protected route: ActivatedRoute, + protected uiNotificationService: UiNotificationService, + protected httpErrorHandlingService: HttpErrorHandlingService, + protected queryParamsService: QueryParamsService, + private TenantService: TenantService, + 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(): TenantLookup { + const lookup = new TenantLookup(); + lookup.metadata = { countAll: true }; + lookup.page = { offset: 0, size: this.ITEMS_PER_PAGE }; + lookup.isActive = [IsActive.Active]; + lookup.order = { items: [this.toDescSortField(nameof(x => x.createdAt))] }; + this.updateOrderUiFields(lookup.order); + + lookup.project = { + fields: this.lookupFields + }; + + return lookup; + } + + protected setupColumns() { + this.gridColumns.push(...[{ + prop: nameof(x => x.name), + sortable: true, + languageName: 'TENANT-LISTING.FIELDS.NAME' + }, { + prop: nameof(x => x.code), + sortable: true, + languageName: 'TENANT-LISTING.FIELDS.CODE', + //cellTemplate: this.TenantStatus + }, + { + prop: nameof(x => x.createdAt), + sortable: true, + languageName: 'TENANT-LISTING.FIELDS.CREATED-AT', + pipe: this.pipeService.getPipe(DataTableDateTimeFormatPipe).withFormat('short') + }, + { + prop: nameof(x => x.updatedAt), + sortable: true, + languageName: 'TENANT-LISTING.FIELDS.UPDATED-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.TenantService.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.TenantService.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.ngOnInit(); + } +} diff --git a/dmp-frontend/src/app/ui/admin/tenant/tenant.module.ts b/dmp-frontend/src/app/ui/admin/tenant/tenant.module.ts new file mode 100644 index 000000000..bd58b40e6 --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/tenant/tenant.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 { UrlListingModule } from '@app/library/url-listing/url-listing.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 { TenantRoutingModule } from './tenant.routing'; +import { TenantEditorComponent } from './editor/tenant-editor.component'; +import { TenantListingComponent } from './listing/tenant-listing.component'; +import { TenantListingFiltersComponent } from "./listing/filters/tenant-listing-filters.component"; +import { RichTextEditorModule } from '@app/library/rich-text-editor/rich-text-editor.module'; + +@NgModule({ + imports: [ + CommonUiModule, + CommonFormsModule, + UrlListingModule, + ConfirmationDialogModule, + TenantRoutingModule, + NgxDropzoneModule, + DragDropModule, + AutoCompleteModule, + HybridListingModule, + TextFilterModule, + UserSettingsModule, + CommonFormattingModule, + RichTextEditorModule + ], + declarations: [ + TenantEditorComponent, + TenantListingComponent, + TenantListingFiltersComponent + ] +}) +export class TenantModule { } diff --git a/dmp-frontend/src/app/ui/admin/tenant/tenant.routing.ts b/dmp-frontend/src/app/ui/admin/tenant/tenant.routing.ts new file mode 100644 index 000000000..4a24a3228 --- /dev/null +++ b/dmp-frontend/src/app/ui/admin/tenant/tenant.routing.ts @@ -0,0 +1,58 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AdminAuthGuard } from '@app/core/admin-auth-guard.service'; +import { TenantEditorComponent } from './editor/tenant-editor.component'; +import { TenantListingComponent } from './listing/tenant-listing.component'; +import { AppPermission } from '@app/core/common/enum/permission.enum'; +import { AuthGuard } from '@app/core/auth-guard.service'; +import { BreadcrumbService } from '@app/ui/misc/breadcrumb/breadcrumb.service'; +import { PendingChangesGuard } from '@common/forms/pending-form-changes/pending-form-changes-guard.service'; +import { TenantEditorResolver } from './editor/tenant-editor.resolver'; + +const routes: Routes = [ + { + path: '', + component: TenantListingComponent, + canActivate: [AuthGuard] + }, + { + path: 'new', + canActivate: [AuthGuard], + component: TenantEditorComponent, + canDeactivate: [PendingChangesGuard], + data: { + authContext: { + permissions: [AppPermission.EditTenant] + }, + ...BreadcrumbService.generateRouteDataConfiguration({ + title: 'BREADCRUMBS.NEW-TENANT' + }) + } + }, + { + path: ':id', + canActivate: [AuthGuard], + component: TenantEditorComponent, + canDeactivate: [PendingChangesGuard], + resolve: { + 'entity': TenantEditorResolver + }, + data: { + ...BreadcrumbService.generateRouteDataConfiguration({ + title: 'BREADCRUMBS.EDIT-TENANT' + }), + authContext: { + permissions: [AppPermission.EditTenant] + } + } + + }, + { path: '**', loadChildren: () => import('@common/modules/page-not-found/page-not-found.module').then(m => m.PageNotFoundModule) }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [TenantEditorResolver] +}) +export class TenantRoutingModule { } diff --git a/dmp-frontend/src/app/ui/sidebar/sidebar.component.ts b/dmp-frontend/src/app/ui/sidebar/sidebar.component.ts index 0c0f9b6d3..3de6e9d34 100644 --- a/dmp-frontend/src/app/ui/sidebar/sidebar.component.ts +++ b/dmp-frontend/src/app/ui/sidebar/sidebar.component.ts @@ -54,6 +54,7 @@ export const ADMIN_ROUTES: RouteInfo[] = [ { path: '/description-templates', title: 'SIDE-BAR.DESCRIPTION-TEMPLATES', icon: 'library_books' }, { path: '/description-template-type', title: 'SIDE-BAR.DESCRIPTION-TEMPLATE-TYPES', icon: 'library_books' }, { path: '/reference-type', title: 'SIDE-BAR.REFERENCE-TYPES', icon: 'library_books' }, + { path: '/tenants', title: 'SIDE-BAR.TENANTS', icon: 'library_books' }, { path: '/users', title: 'SIDE-BAR.USERS', icon: 'people' }, { path: '/language-editor', title: 'SIDE-BAR.LANGUAGE-EDITOR', icon: 'language' }, { path: '/supportive-material', title: 'SIDE-BAR.SUPPORTIVE-MATERIAL', icon: 'import_contacts' } diff --git a/dmp-frontend/src/assets/i18n/en.json b/dmp-frontend/src/assets/i18n/en.json index ee4b62614..9ec441a51 100644 --- a/dmp-frontend/src/assets/i18n/en.json +++ b/dmp-frontend/src/assets/i18n/en.json @@ -172,7 +172,8 @@ "GUIDE-EDITOR": "User Guide Editor", "LANGUAGE": "Language", "SIGN-IN": "Sign in to account", - "REFERENCE-TYPE": "Reference Types" + "REFERENCE-TYPES": "Reference Types", + "TENANTS": "Tenants" }, "FILE-TYPES": { "PDF": "PDF", @@ -241,7 +242,9 @@ "NEW-DESCRIPTION-TEMPLATES": "New", "EDIT-DESCRIPTION-TEMPLATES": "Edit", "NEW-REFERENCE-TYPE": "New", - "EDIT-REFERENCE-TYPE": "Edit" + "EDIT-REFERENCE-TYPE": "Edit", + "NEW-TENANT": "New", + "EDIT-TENANT": "Edit" }, "COOKIE": { "MESSAGE": "This website uses cookies to enhance the user experience.", @@ -336,7 +339,8 @@ "SUPPORT": "Support", "FEEDBACK": "Send feedback", "SUPPORTIVE-MATERIAL": "Supportive Material", - "REFERENCE-TYPES":"Reference Types" + "REFERENCE-TYPES":"Reference Types", + "TENANTS": "Tenants" }, "DATASET-PROFILE-EDITOR": { "TITLE": { @@ -1078,6 +1082,33 @@ "SUCCESSFUL-DELETE": "Successful Delete", "UNSUCCESSFUL-DELETE": "This item could not be deleted." }, + "TENANT-LISTING": { + "TITLE": "Tenants", + "CREATE": "Create Tenant", + "FIELDS": { + "NAME": "Name", + "CODE": "Code", + "UPDATED-AT": "Updated", + "CREATED-AT": "Created" + }, + "FILTER": { + "TITLE": "Filters", + "IS-ACTIVE": "Is Active", + "CANCEL": "Cancel", + "APPLY-FILTERS": "Apply filters" + }, + "CONFIRM-DELETE-DIALOG": { + "MESSAGE": "Would you like to delete this Tenant?", + "CONFIRM-BUTTON": "Yes, delete", + "CANCEL-BUTTON": "No" + }, + "ACTIONS": { + "DELETE": "Delete", + "EDIT": "Edit" + }, + "SUCCESSFUL-DELETE": "Successful Delete", + "UNSUCCESSFUL-DELETE": "This item could not be deleted." + }, "DATASET-UPLOAD": { "TITLE": "Import Dataset", "UPLOAD-BUTTON": "Upload", @@ -1169,6 +1200,24 @@ "CANCEL-BUTTON": "No" } }, + "TENANT-EDITOR": { + "NEW": "New Tenant", + "FIELDS": { + "NAME": "Name", + "CODE": "Code", + "DESCRIPTION": "Description" + }, + "ACTIONS": { + "SAVE": "Save", + "CANCEL": "Cancel", + "DELETE": "Delete" + }, + "CONFIRM-DELETE-DIALOG": { + "MESSAGE": "Would you like to delete this Tenant?", + "CONFIRM-BUTTON": "Yes, delete", + "CANCEL-BUTTON": "No" + } + }, "DMP-BLUEPRINT-EDITOR": { "TITLE": { "NEW": "New DMP Blueprint",