diff --git a/backend/core/src/main/java/org/opencdmp/authorization/Permission.java b/backend/core/src/main/java/org/opencdmp/authorization/Permission.java index d9e50a204..e4a4762a4 100644 --- a/backend/core/src/main/java/org/opencdmp/authorization/Permission.java +++ b/backend/core/src/main/java/org/opencdmp/authorization/Permission.java @@ -15,6 +15,9 @@ public final class Permission { public static String PublicBrowseDashboardStatistics = "PublicBrowseDashboardStatistics"; public static String PublicSendContactSupport = "PublicSendContactSupport"; public static String PublicBrowseReferenceType = "PublicBrowseReferenceType"; + public static String PublicClonePlan = "PublicClonePlan"; + public static String PublicCloneDescription = "PublicCloneDescription"; + //Elastic public static String ManageElastic = "ManageElastic"; //Queue Events diff --git a/backend/core/src/main/java/org/opencdmp/commons/enums/UsageLimitMetricValue.java b/backend/core/src/main/java/org/opencdmp/commons/enums/UsageLimitTargetMetric.java similarity index 54% rename from backend/core/src/main/java/org/opencdmp/commons/enums/UsageLimitMetricValue.java rename to backend/core/src/main/java/org/opencdmp/commons/enums/UsageLimitTargetMetric.java index 8d7e7a171..0a615ddbe 100644 --- a/backend/core/src/main/java/org/opencdmp/commons/enums/UsageLimitMetricValue.java +++ b/backend/core/src/main/java/org/opencdmp/commons/enums/UsageLimitTargetMetric.java @@ -5,18 +5,18 @@ import org.opencdmp.data.converters.enums.DatabaseEnum; import java.util.Map; -public enum UsageLimitMetricValue implements DatabaseEnum { - USER_COUNT(MetricValues.UserCount), - PLAN_COUNT(MetricValues.PlanCount), - BLUEPRINT_COUNT(MetricValues.BlueprintCount), - DESCRIPTION_COUNT(MetricValues.DescriptionCount), - DESCRIPTION_TEMPLATE_COUNT(MetricValues.DescriptionTemplateCount), - DESCRIPTION_TEMPLATE_TYPE_COUNT(MetricValues.DescriptionTemplateTypeCount), - PREFILLING_SOURCES_COUNT(MetricValues.PrefillingSourcesCount), - REFERENCE_TYPE_COUNT(MetricValues.ReferenceTypeCount); +public enum UsageLimitTargetMetric implements DatabaseEnum { + USER_COUNT(TargetMetrics.UserCount), + PLAN_COUNT(TargetMetrics.PlanCount), + BLUEPRINT_COUNT(TargetMetrics.BlueprintCount), + DESCRIPTION_COUNT(TargetMetrics.DescriptionCount), + DESCRIPTION_TEMPLATE_COUNT(TargetMetrics.DescriptionTemplateCount), + DESCRIPTION_TEMPLATE_TYPE_COUNT(TargetMetrics.DescriptionTemplateTypeCount), + PREFILLING_SOURCES_COUNT(TargetMetrics.PrefillingSourcesCount), + REFERENCE_TYPE_COUNT(TargetMetrics.ReferenceTypeCount); private final String value; - public static class MetricValues { + public static class TargetMetrics { public static final String UserCount = "user_count"; public static final String PlanCount = "plan_count"; public static final String BlueprintCount = "blueprint_count"; @@ -27,7 +27,7 @@ public enum UsageLimitMetricValue implements DatabaseEnum { public static final String ReferenceTypeCount = "reference_type_count"; } - UsageLimitMetricValue(String value) { + UsageLimitTargetMetric(String value) { this.value = value; } @@ -36,9 +36,9 @@ public enum UsageLimitMetricValue implements DatabaseEnum { return this.value; } - private static final Map map = EnumUtils.getEnumValueMap(UsageLimitMetricValue.class); + private static final Map map = EnumUtils.getEnumValueMap(UsageLimitTargetMetric.class); - public static UsageLimitMetricValue of(String i) { + public static UsageLimitTargetMetric of(String i) { return map.get(i); } } diff --git a/backend/core/src/main/java/org/opencdmp/data/UsageLimitEntity.java b/backend/core/src/main/java/org/opencdmp/data/UsageLimitEntity.java index 3598ad8bd..a848ededf 100644 --- a/backend/core/src/main/java/org/opencdmp/data/UsageLimitEntity.java +++ b/backend/core/src/main/java/org/opencdmp/data/UsageLimitEntity.java @@ -2,8 +2,9 @@ package org.opencdmp.data; import jakarta.persistence.*; import org.opencdmp.commons.enums.IsActive; -import org.opencdmp.commons.enums.UsageLimitMetricValue; +import org.opencdmp.commons.enums.UsageLimitTargetMetric; import org.opencdmp.data.converters.enums.IsActiveConverter; +import org.opencdmp.data.converters.enums.UsageLimitTargetMetricConverter; import org.opencdmp.data.tenant.TenantScopedBaseEntity; import java.time.Instant; @@ -27,10 +28,11 @@ public class UsageLimitEntity extends TenantScopedBaseEntity { public static final int _labelLength = 250; - @Column(name = "metric_value", nullable = false) - private UsageLimitMetricValue metricValue; + @Column(name = "target_metric", nullable = false) + @Convert(converter = UsageLimitTargetMetricConverter.class) + private UsageLimitTargetMetric targetMetric; - public static final String _metricValue = "metricValue"; + public static final String _targetMetric = "targetMetric"; @Column(name = "value", nullable = false) private Long value; @@ -69,12 +71,12 @@ public class UsageLimitEntity extends TenantScopedBaseEntity { this.label = label; } - public UsageLimitMetricValue getMetricValue() { - return metricValue; + public UsageLimitTargetMetric getTargetMetric() { + return targetMetric; } - public void setMetricValue(UsageLimitMetricValue metricValue) { - this.metricValue = metricValue; + public void setTargetMetric(UsageLimitTargetMetric targetMetric) { + this.targetMetric = targetMetric; } public Long getValue() { diff --git a/backend/core/src/main/java/org/opencdmp/data/converters/enums/UsageLimitTargetMetricConverter.java b/backend/core/src/main/java/org/opencdmp/data/converters/enums/UsageLimitTargetMetricConverter.java new file mode 100644 index 000000000..d7163fdc6 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/data/converters/enums/UsageLimitTargetMetricConverter.java @@ -0,0 +1,11 @@ +package org.opencdmp.data.converters.enums; + +import jakarta.persistence.Converter; +import org.opencdmp.commons.enums.UsageLimitTargetMetric; + +@Converter +public class UsageLimitTargetMetricConverter extends DatabaseEnumConverter { + public UsageLimitTargetMetric of(String i) { + return UsageLimitTargetMetric.of(i); + } +} diff --git a/backend/core/src/main/java/org/opencdmp/errorcode/ErrorThesaurusProperties.java b/backend/core/src/main/java/org/opencdmp/errorcode/ErrorThesaurusProperties.java index 070710b6a..8baa57b70 100644 --- a/backend/core/src/main/java/org/opencdmp/errorcode/ErrorThesaurusProperties.java +++ b/backend/core/src/main/java/org/opencdmp/errorcode/ErrorThesaurusProperties.java @@ -388,4 +388,14 @@ public class ErrorThesaurusProperties { public void setMaxDescriptionsExceeded(ErrorDescription maxDescriptionsExceeded) { this.maxDescriptionsExceeded = maxDescriptionsExceeded; } + + private ErrorDescription usageLimitException; + + public ErrorDescription getUsageLimitException() { + return usageLimitException; + } + + public void setUsageLimitException(ErrorDescription usageLimitException) { + this.usageLimitException = usageLimitException; + } } diff --git a/backend/core/src/main/java/org/opencdmp/model/PublicReference.java b/backend/core/src/main/java/org/opencdmp/model/PublicReference.java index ba10bf426..f50487b21 100644 --- a/backend/core/src/main/java/org/opencdmp/model/PublicReference.java +++ b/backend/core/src/main/java/org/opencdmp/model/PublicReference.java @@ -1,5 +1,7 @@ package org.opencdmp.model; +import org.opencdmp.commons.enums.ReferenceSourceType; + import java.util.UUID; public class PublicReference { @@ -19,6 +21,13 @@ public class PublicReference { private String reference; public static final String _reference = "reference"; + private String source; + public static final String _source = "source"; + + private ReferenceSourceType sourceType; + public static final String _sourceType = "sourceType"; + + public UUID getId() { return id; } @@ -58,6 +67,22 @@ public class PublicReference { public void setReference(String reference) { this.reference = reference; } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public ReferenceSourceType getSourceType() { + return sourceType; + } + + public void setSourceType(ReferenceSourceType sourceType) { + this.sourceType = sourceType; + } } diff --git a/backend/core/src/main/java/org/opencdmp/model/UsageLimit.java b/backend/core/src/main/java/org/opencdmp/model/UsageLimit.java index 5ddfff509..72d00761c 100644 --- a/backend/core/src/main/java/org/opencdmp/model/UsageLimit.java +++ b/backend/core/src/main/java/org/opencdmp/model/UsageLimit.java @@ -1,7 +1,7 @@ package org.opencdmp.model; import org.opencdmp.commons.enums.IsActive; -import org.opencdmp.commons.enums.UsageLimitMetricValue; +import org.opencdmp.commons.enums.UsageLimitTargetMetric; import java.time.Instant; import java.util.UUID; @@ -14,8 +14,8 @@ public class UsageLimit { private String label; public static final String _label = "label"; - private UsageLimitMetricValue metricValue; - public static final String _metricValue = "metricValue"; + private UsageLimitTargetMetric targetMetric; + public static final String _targetMetric = "targetMetric"; private Long value; public static final String _value = "value"; @@ -51,12 +51,12 @@ public class UsageLimit { this.label = label; } - public UsageLimitMetricValue getMetricValue() { - return metricValue; + public UsageLimitTargetMetric getTargetMetric() { + return targetMetric; } - public void setMetricValue(UsageLimitMetricValue metricValue) { - this.metricValue = metricValue; + public void setTargetMetric(UsageLimitTargetMetric targetMetric) { + this.targetMetric = targetMetric; } public Long getValue() { diff --git a/backend/core/src/main/java/org/opencdmp/model/builder/PublicReferenceBuilder.java b/backend/core/src/main/java/org/opencdmp/model/builder/PublicReferenceBuilder.java index 9c40c0383..0fade32f4 100644 --- a/backend/core/src/main/java/org/opencdmp/model/builder/PublicReferenceBuilder.java +++ b/backend/core/src/main/java/org/opencdmp/model/builder/PublicReferenceBuilder.java @@ -13,6 +13,7 @@ import org.opencdmp.convention.ConventionService; import org.opencdmp.data.ReferenceEntity; import org.opencdmp.model.PublicReference; import org.opencdmp.model.PublicReferenceType; +import org.opencdmp.model.reference.Reference; import org.opencdmp.query.ReferenceTypeQuery; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -62,6 +63,8 @@ public class PublicReferenceBuilder extends BaseBuilder m.setId(d.getId()); if (fields.hasField(this.asIndexer(UsageLimit._label))) m.setLabel(d.getLabel()); - if (fields.hasField(this.asIndexer(UsageLimit._metricValue))) - m.setMetricValue(d.getMetricValue()); + if (fields.hasField(this.asIndexer(UsageLimit._targetMetric))) + m.setTargetMetric(d.getTargetMetric()); if (fields.hasField(this.asIndexer(UsageLimit._value))) m.setValue(d.getValue()); if (fields.hasField(this.asIndexer(UsageLimit._createdAt))) diff --git a/backend/core/src/main/java/org/opencdmp/model/persist/UsageLimitPersist.java b/backend/core/src/main/java/org/opencdmp/model/persist/UsageLimitPersist.java index 7b0bccb64..7d5ea711b 100644 --- a/backend/core/src/main/java/org/opencdmp/model/persist/UsageLimitPersist.java +++ b/backend/core/src/main/java/org/opencdmp/model/persist/UsageLimitPersist.java @@ -1,7 +1,7 @@ package org.opencdmp.model.persist; import gr.cite.tools.validation.specification.Specification; -import org.opencdmp.commons.enums.UsageLimitMetricValue; +import org.opencdmp.commons.enums.UsageLimitTargetMetric; import org.opencdmp.commons.validation.BaseValidator; import org.opencdmp.convention.ConventionService; import org.opencdmp.data.UsageLimitEntity; @@ -24,8 +24,8 @@ public class UsageLimitPersist { private String label; public static final String _label = "label"; - private UsageLimitMetricValue metricValue;; - public static final String _metricValue = "metricValue"; + private UsageLimitTargetMetric targetMetric;; + public static final String _targetMetric = "targetMetric"; private Long value; public static final String _value = "value"; @@ -49,12 +49,12 @@ public class UsageLimitPersist { this.label = label; } - public UsageLimitMetricValue getMetricValue() { - return metricValue; + public UsageLimitTargetMetric getTargetMetric() { + return targetMetric; } - public void setMetricValue(UsageLimitMetricValue metricValue) { - this.metricValue = metricValue; + public void setTargetMetric(UsageLimitTargetMetric targetMetric) { + this.targetMetric = targetMetric; } public Long getValue() { @@ -110,11 +110,14 @@ public class UsageLimitPersist { .must(() -> this.lessEqualLength(item.getLabel(), UsageLimitEntity._labelLength)) .failOn(UsageLimitPersist._label).failWith(this.messageSource.getMessage("Validation_MaxLength", new Object[]{UsageLimitPersist._label}, LocaleContextHolder.getLocale())), this.spec() - .must(() -> !this.isNull(item.getMetricValue())) - .failOn(UsageLimitPersist._metricValue).failWith(this.messageSource.getMessage("Validation_Required", new Object[]{UsageLimitPersist._metricValue}, LocaleContextHolder.getLocale())), + .must(() -> !this.isNull(item.getTargetMetric())) + .failOn(UsageLimitPersist._targetMetric).failWith(this.messageSource.getMessage("Validation_Required", new Object[]{UsageLimitPersist._targetMetric}, LocaleContextHolder.getLocale())), this.spec() .must(() -> !this.isNull(item.getValue())) - .failOn(UsageLimitPersist._value).failWith(this.messageSource.getMessage("Validation_Required", new Object[]{UsageLimitPersist._value}, LocaleContextHolder.getLocale())) + .failOn(UsageLimitPersist._value).failWith(this.messageSource.getMessage("Validation_Required", new Object[]{UsageLimitPersist._value}, LocaleContextHolder.getLocale())), + this.spec() + .must(() -> item.getValue() > 0) + .failOn(UsageLimitPersist._value).failWith(this.messageSource.getMessage("Validation_UnexpectedValue", new Object[]{UsageLimitPersist._value}, LocaleContextHolder.getLocale())) ); } } diff --git a/backend/core/src/main/java/org/opencdmp/query/UsageLimitQuery.java b/backend/core/src/main/java/org/opencdmp/query/UsageLimitQuery.java index 74092c48a..fc5a4e88f 100644 --- a/backend/core/src/main/java/org/opencdmp/query/UsageLimitQuery.java +++ b/backend/core/src/main/java/org/opencdmp/query/UsageLimitQuery.java @@ -10,7 +10,7 @@ import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.Predicate; import org.opencdmp.authorization.AuthorizationFlags; import org.opencdmp.commons.enums.IsActive; -import org.opencdmp.commons.enums.UsageLimitMetricValue; +import org.opencdmp.commons.enums.UsageLimitTargetMetric; import org.opencdmp.commons.scope.user.UserScope; import org.opencdmp.data.*; import org.opencdmp.model.UsageLimit; @@ -32,7 +32,7 @@ public class UsageLimitQuery extends QueryBase { private Collection isActives; - private Collection usageLimitMetricValues; + private Collection usageLimitTargetMetrics; private Collection excludedIds; @@ -88,18 +88,18 @@ public class UsageLimitQuery extends QueryBase { return this; } - public UsageLimitQuery usageLimitMetricValues(UsageLimitMetricValue value) { - this.usageLimitMetricValues = List.of(value); + public UsageLimitQuery usageLimitTargetMetrics(UsageLimitTargetMetric value) { + this.usageLimitTargetMetrics = List.of(value); return this; } - public UsageLimitQuery usageLimitMetricValues(UsageLimitMetricValue... value) { - this.usageLimitMetricValues = Arrays.asList(value); + public UsageLimitQuery usageLimitTargetMetrics(UsageLimitTargetMetric... value) { + this.usageLimitTargetMetrics = Arrays.asList(value); return this; } - public UsageLimitQuery usageLimitMetricValues(Collection values) { - this.usageLimitMetricValues = values; + public UsageLimitQuery usageLimitTargetMetrics(Collection values) { + this.usageLimitTargetMetrics = values; return this; } @@ -145,7 +145,7 @@ public class UsageLimitQuery extends QueryBase { @Override protected Boolean isFalseQuery() { - return this.isEmpty(this.ids) || this.isEmpty(this.isActives) || this.isEmpty(this.excludedIds) || this.isEmpty(this.usageLimitMetricValues); + return this.isEmpty(this.ids) || this.isEmpty(this.isActives) || this.isEmpty(this.excludedIds) || this.isEmpty(this.usageLimitTargetMetrics); } @Override @@ -166,9 +166,9 @@ public class UsageLimitQuery extends QueryBase { inClause.value(item); predicates.add(inClause); } - if (this.usageLimitMetricValues != null) { - CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(UsageLimitEntity._metricValue)); - for (UsageLimitMetricValue item : this.usageLimitMetricValues) + if (this.usageLimitTargetMetrics != null) { + CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(UsageLimitEntity._targetMetric)); + for (UsageLimitTargetMetric item : this.usageLimitTargetMetrics) inClause.value(item); predicates.add(inClause); } @@ -192,7 +192,7 @@ public class UsageLimitQuery extends QueryBase { item.setId(QueryBase.convertSafe(tuple, columns, UsageLimitEntity._id, UUID.class)); item.setTenantId(QueryBase.convertSafe(tuple, columns, UsageLimitEntity._tenantId, UUID.class)); item.setLabel(QueryBase.convertSafe(tuple, columns, UsageLimitEntity._label, String.class)); - item.setMetricValue(QueryBase.convertSafe(tuple, columns, UsageLimitEntity._metricValue, UsageLimitMetricValue.class)); + item.setTargetMetric(QueryBase.convertSafe(tuple, columns, UsageLimitEntity._targetMetric, UsageLimitTargetMetric.class)); item.setValue(QueryBase.convertSafe(tuple, columns, UsageLimitEntity._value, Long.class)); item.setCreatedAt(QueryBase.convertSafe(tuple, columns, UsageLimitEntity._createdAt, Instant.class)); item.setUpdatedAt(QueryBase.convertSafe(tuple, columns, UsageLimitEntity._updatedAt, Instant.class)); @@ -206,8 +206,8 @@ public class UsageLimitQuery extends QueryBase { return UsageLimitEntity._id; else if (item.match(UsageLimit._label)) return UsageLimitEntity._label; - else if (item.match(UsageLimit._metricValue)) - return UsageLimitEntity._metricValue; + else if (item.match(UsageLimit._targetMetric)) + return UsageLimitEntity._targetMetric; else if (item.match(UsageLimit._value)) return UsageLimitEntity._value; else if (item.match(UsageLimit._createdAt)) diff --git a/backend/core/src/main/java/org/opencdmp/query/lookup/UsageLimitLookup.java b/backend/core/src/main/java/org/opencdmp/query/lookup/UsageLimitLookup.java index cb32432d8..3bed4ce28 100644 --- a/backend/core/src/main/java/org/opencdmp/query/lookup/UsageLimitLookup.java +++ b/backend/core/src/main/java/org/opencdmp/query/lookup/UsageLimitLookup.java @@ -3,7 +3,7 @@ package org.opencdmp.query.lookup; import gr.cite.tools.data.query.Lookup; import gr.cite.tools.data.query.QueryFactory; import org.opencdmp.commons.enums.IsActive; -import org.opencdmp.commons.enums.UsageLimitMetricValue; +import org.opencdmp.commons.enums.UsageLimitTargetMetric; import org.opencdmp.query.UsageLimitQuery; import java.util.List; @@ -14,7 +14,7 @@ public class UsageLimitLookup extends Lookup { private String like; private List isActive; private List ids; - private List usageLimitMetricValues; + private List usageLimitTargetMetrics; private List excludedIds; public String getLike() { @@ -39,12 +39,12 @@ public class UsageLimitLookup extends Lookup { public void setIds(List ids) { this.ids = ids; } - public List getUsageLimitsMetricValues() { - return usageLimitMetricValues; + public List getUsageLimitTargetMetrics() { + return usageLimitTargetMetrics; } - public void setUsageLimitsMetricValues(List usageLimitMetricValues) { - this.usageLimitMetricValues = usageLimitMetricValues; + public void setUsageLimitTargetMetrics(List usageLimitTargetMetrics) { + this.usageLimitTargetMetrics = usageLimitTargetMetrics; } public List getExcludedIds() { @@ -60,7 +60,7 @@ public class UsageLimitLookup extends Lookup { 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.usageLimitMetricValues != null) query.usageLimitMetricValues(this.usageLimitMetricValues); + if (this.usageLimitTargetMetrics != null) query.usageLimitTargetMetrics(this.usageLimitTargetMetrics); if (this.excludedIds != null) query.excludedIds(this.excludedIds); this.enrichCommon(query); diff --git a/backend/core/src/main/java/org/opencdmp/service/accounting/AccountingService.java b/backend/core/src/main/java/org/opencdmp/service/accounting/AccountingService.java index e1ad947e3..802b56c05 100644 --- a/backend/core/src/main/java/org/opencdmp/service/accounting/AccountingService.java +++ b/backend/core/src/main/java/org/opencdmp/service/accounting/AccountingService.java @@ -1,26 +1,14 @@ package org.opencdmp.service.accounting; -public class AccountingService { +import org.opencdmp.commons.enums.UsageLimitTargetMetric; - private Integer getCurrentMetricValue(String metric) { - return 10; - } +public interface AccountingService { - private void set(String metric) { - //Get/Calculate current metric value - //Find metric value from db - // compare these two and throw UsageLimitException when current > metric value - } + Integer getCurrentMetricValue(UsageLimitTargetMetric metric); - private void increase(String metric) { - //Get/Calculate current metric value - //Find metric value from db - // compare these two and throw UsageLimitException when current > metric value - } + void set(UsageLimitTargetMetric metric); - private void decrease(String metric) { - //Get/Calculate current metric value - //Find metric value from db - // compare these two and throw UsageLimitException when current > metric value - } + void increase(UsageLimitTargetMetric metric); + + void decrease(UsageLimitTargetMetric metric); } diff --git a/backend/core/src/main/java/org/opencdmp/service/accounting/AccountingServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/accounting/AccountingServiceImpl.java new file mode 100644 index 000000000..262a74dc3 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/service/accounting/AccountingServiceImpl.java @@ -0,0 +1,54 @@ +package org.opencdmp.service.accounting; + +import gr.cite.commons.web.authz.service.AuthorizationService; +import gr.cite.tools.logging.LoggerService; +import org.opencdmp.commons.enums.UsageLimitTargetMetric; +import org.opencdmp.convention.ConventionService; +import org.opencdmp.errorcode.ErrorThesaurusProperties; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Service; + +@Service +public class AccountingServiceImpl implements AccountingService { + + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(AccountingServiceImpl.class)); + private final AuthorizationService authorizationService; + private final ConventionService conventionService; + + private final ErrorThesaurusProperties errors; + + private final MessageSource messageSource; + + @Autowired + public AccountingServiceImpl( + AuthorizationService authorizationService, + ConventionService conventionService, + ErrorThesaurusProperties errors, + MessageSource messageSource) { + this.authorizationService = authorizationService; + this.conventionService = conventionService; + this.errors = errors; + this.messageSource = messageSource; + } + + public Integer getCurrentMetricValue(UsageLimitTargetMetric metric) { + //TODO + //Get/Calculate current metric value from accountingService + return 10; + } + + public void set(UsageLimitTargetMetric metric) { + //TODO + } + + public void increase(UsageLimitTargetMetric metric) { + //TODO + } + + public void decrease(UsageLimitTargetMetric metric) { + //TODO + } +} + diff --git a/backend/core/src/main/java/org/opencdmp/service/descriptiontemplatetype/DescriptionTemplateTypeServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/descriptiontemplatetype/DescriptionTemplateTypeServiceImpl.java index eb01477ed..738172b00 100644 --- a/backend/core/src/main/java/org/opencdmp/service/descriptiontemplatetype/DescriptionTemplateTypeServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/descriptiontemplatetype/DescriptionTemplateTypeServiceImpl.java @@ -14,6 +14,7 @@ import gr.cite.tools.logging.MapLogEntry; import org.opencdmp.authorization.AuthorizationFlags; import org.opencdmp.authorization.Permission; import org.opencdmp.commons.enums.IsActive; +import org.opencdmp.commons.enums.UsageLimitTargetMetric; import org.opencdmp.convention.ConventionService; import org.opencdmp.data.DescriptionTemplateTypeEntity; import org.opencdmp.data.TenantEntityManager; @@ -24,6 +25,8 @@ import org.opencdmp.model.DescriptionTemplateType; import org.opencdmp.model.builder.DescriptionTemplateTypeBuilder; import org.opencdmp.model.deleter.DescriptionTemplateTypeDeleter; import org.opencdmp.model.persist.DescriptionTemplateTypePersist; +import org.opencdmp.service.accounting.AccountingService; +import org.opencdmp.service.usagelimit.UsageLimitServiceImpl; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; @@ -56,6 +59,10 @@ public class DescriptionTemplateTypeServiceImpl implements DescriptionTemplateTy private final EventBroker eventBroker; + private final UsageLimitServiceImpl usageLimitService; + + private final AccountingService accountingService; + @Autowired public DescriptionTemplateTypeServiceImpl( TenantEntityManager entityManager, @@ -65,7 +72,7 @@ public class DescriptionTemplateTypeServiceImpl implements DescriptionTemplateTy ConventionService conventionService, ErrorThesaurusProperties errors, MessageSource messageSource, - EventBroker eventBroker) { + EventBroker eventBroker, UsageLimitServiceImpl usageLimitService, AccountingService accountingService) { this.entityManager = entityManager; this.authorizationService = authorizationService; this.deleterFactory = deleterFactory; @@ -74,6 +81,8 @@ public class DescriptionTemplateTypeServiceImpl implements DescriptionTemplateTy this.errors = errors; this.messageSource = messageSource; this.eventBroker = eventBroker; + this.usageLimitService = usageLimitService; + this.accountingService = accountingService; } public DescriptionTemplateType persist(DescriptionTemplateTypePersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException { @@ -89,6 +98,7 @@ public class DescriptionTemplateTypeServiceImpl implements DescriptionTemplateTy if (data == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{model.getId(), DescriptionTemplateType.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 { + this.usageLimitService.checkIncrease(UsageLimitTargetMetric.DESCRIPTION_TEMPLATE_TYPE_COUNT); data = new DescriptionTemplateTypeEntity(); data.setId(UUID.randomUUID()); data.setIsActive(IsActive.Active); @@ -100,8 +110,10 @@ public class DescriptionTemplateTypeServiceImpl implements DescriptionTemplateTy data.setUpdatedAt(Instant.now()); if (isUpdate) this.entityManager.merge(data); - else + else{ this.entityManager.persist(data); + this.accountingService.increase(UsageLimitTargetMetric.DESCRIPTION_TEMPLATE_TYPE_COUNT); + } this.entityManager.flush(); @@ -115,6 +127,7 @@ public class DescriptionTemplateTypeServiceImpl implements DescriptionTemplateTy this.authorizationService.authorizeForce(Permission.DeleteDescriptionTemplateType); this.deleterFactory.deleter(DescriptionTemplateTypeDeleter.class).deleteAndSaveByIds(List.of(id)); + this.accountingService.decrease(UsageLimitTargetMetric.DESCRIPTION_TEMPLATE_TYPE_COUNT); } } diff --git a/backend/core/src/main/java/org/opencdmp/service/plan/PlanServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/plan/PlanServiceImpl.java index bd29d1cef..f4ef70fb6 100644 --- a/backend/core/src/main/java/org/opencdmp/service/plan/PlanServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/plan/PlanServiceImpl.java @@ -71,6 +71,7 @@ import org.opencdmp.integrationevent.outbox.notification.NotifyIntegrationEvent; import org.opencdmp.integrationevent.outbox.notification.NotifyIntegrationEventHandler; import org.opencdmp.model.PlanUser; import org.opencdmp.model.PlanValidationResult; +import org.opencdmp.model.PublicPlan; import org.opencdmp.model.builder.PlanUserBuilder; import org.opencdmp.model.builder.description.DescriptionBuilder; import org.opencdmp.model.builder.plan.PlanBuilder; @@ -91,6 +92,7 @@ import org.opencdmp.model.planblueprint.PlanBlueprint; import org.opencdmp.model.planreference.PlanReferenceData; import org.opencdmp.model.reference.Reference; import org.opencdmp.model.referencetype.ReferenceType; +import org.opencdmp.model.result.QueryResult; import org.opencdmp.query.*; import org.opencdmp.service.actionconfirmation.ActionConfirmationService; import org.opencdmp.service.description.DescriptionService; @@ -125,6 +127,8 @@ import java.time.Instant; import java.util.*; import java.util.stream.Collectors; +import static org.opencdmp.authorization.AuthorizationFlags.Public; + @Service public class PlanServiceImpl implements PlanService { @@ -568,10 +572,12 @@ public class PlanServiceImpl implements PlanService { public void cloneDescription(UUID planId, Map planDescriptionTemplateRemap, UUID descriptionId, UUID newPlanDescriptionTemplateId) throws InvalidApplicationException, IOException { logger.debug("cloning description: {} with description: {}", descriptionId, planId); - this.authorizationService.authorizeAtLeastOneForce(List.of(this.authorizationContentResolver.descriptionAffiliation(descriptionId)), Permission.CloneDescription); + PlanEntity descriptionPlan = this.queryFactory.query(PlanQuery.class).disableTracking().ids(planId).isActive(IsActive.Active).first(); + + if (!descriptionPlan.getAccessType().equals(PlanAccessType.Public)) this.authorizationService.authorizeAtLeastOneForce(List.of(this.authorizationContentResolver.descriptionAffiliation(descriptionId)), Permission.CloneDescription); + else this.authorizationService.authorizeAtLeastOneForce(List.of(this.authorizationContentResolver.descriptionAffiliation(descriptionId)), Permission.PublicCloneDescription); DescriptionEntity existing = this.queryFactory.query(DescriptionQuery.class).disableTracking().ids(descriptionId).isActive(IsActive.Active).first(); - DescriptionEntity newDescription = new DescriptionEntity(); newDescription.setId(UUID.randomUUID()); newDescription.setLabel(existing.getLabel()); @@ -673,12 +679,15 @@ public class PlanServiceImpl implements PlanService { @Override public Plan buildClone(ClonePlanPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, IOException, InvalidApplicationException { - this.authorizationService.authorizeAtLeastOneForce(List.of(this.authorizationContentResolver.planAffiliation( model.getId())), Permission.ClonePlan); - PlanEntity existingPlanEntity = this.queryFactory.query(PlanQuery.class).disableTracking().authorize(AuthorizationFlags.AllExceptPublic).ids(model.getId()).firstAs(fields); + PlanEntity existingPlanEntity = this.queryFactory.query(PlanQuery.class).disableTracking().ids(model.getId()).firstAs(fields); + if (!this.conventionService.isValidGuid(model.getId()) || existingPlanEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{model.getId(), Plan.class.getSimpleName()}, LocaleContextHolder.getLocale())); + if (!existingPlanEntity.getAccessType().equals(PlanAccessType.Public)) this.authorizationService.authorizeAtLeastOneForce(List.of(this.authorizationContentResolver.planAffiliation( model.getId())), Permission.ClonePlan); + else this.authorizationService.authorizeAtLeastOneForce(List.of(this.authorizationContentResolver.planAffiliation( model.getId())), Permission.PublicClonePlan); + PlanEntity newPlan = new PlanEntity(); newPlan.setId(UUID.randomUUID()); newPlan.setIsActive(IsActive.Active); @@ -711,6 +720,20 @@ public class PlanServiceImpl implements PlanService { .isActive(IsActive.Active) .collect(); + UUID currentUserId = this.userScope.getUserId(); + boolean currentUserIsInPlan = planUsers.stream().anyMatch(u -> u.getUserId() == currentUserId); + if (!currentUserIsInPlan) { + PlanUserEntity newUser = new PlanUserEntity(); + newUser.setId(UUID.randomUUID()); + newUser.setPlanId(newPlan.getId()); + newUser.setUserId(currentUserId); + newUser.setRole(PlanUserRole.Owner); + newUser.setCreatedAt(Instant.now()); + newUser.setUpdatedAt(Instant.now()); + newUser.setIsActive(IsActive.Active); + + this.entityManager.persist(newUser); + } for (PlanUserEntity planUser : planUsers) { PlanUserEntity newUser = new PlanUserEntity(); newUser.setId(UUID.randomUUID()); diff --git a/backend/core/src/main/java/org/opencdmp/service/usagelimit/UsageLimitService.java b/backend/core/src/main/java/org/opencdmp/service/usagelimit/UsageLimitService.java index 07c74d2ee..4de0d621e 100644 --- a/backend/core/src/main/java/org/opencdmp/service/usagelimit/UsageLimitService.java +++ b/backend/core/src/main/java/org/opencdmp/service/usagelimit/UsageLimitService.java @@ -5,7 +5,7 @@ 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 org.opencdmp.commons.enums.UsageLimitMetricValue; +import org.opencdmp.commons.enums.UsageLimitTargetMetric; import org.opencdmp.model.UsageLimit; import org.opencdmp.model.persist.UsageLimitPersist; @@ -18,6 +18,6 @@ public interface UsageLimitService { void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException; - void checkIncrease(UsageLimitMetricValue metric); + void checkIncrease(UsageLimitTargetMetric metric); } diff --git a/backend/core/src/main/java/org/opencdmp/service/usagelimit/UsageLimitServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/usagelimit/UsageLimitServiceImpl.java index d14b54ec9..c55cd5725 100644 --- a/backend/core/src/main/java/org/opencdmp/service/usagelimit/UsageLimitServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/usagelimit/UsageLimitServiceImpl.java @@ -14,17 +14,19 @@ import gr.cite.tools.logging.MapLogEntry; import org.opencdmp.authorization.AuthorizationFlags; import org.opencdmp.authorization.Permission; import org.opencdmp.commons.enums.IsActive; -import org.opencdmp.commons.enums.UsageLimitMetricValue; -import org.opencdmp.commons.scope.user.UserScope; +import org.opencdmp.commons.enums.UsageLimitTargetMetric; +import gr.cite.tools.data.query.QueryFactory; import org.opencdmp.convention.ConventionService; import org.opencdmp.data.TenantEntityManager; import org.opencdmp.data.UsageLimitEntity; import org.opencdmp.errorcode.ErrorThesaurusProperties; -import org.opencdmp.model.Tag; import org.opencdmp.model.UsageLimit; import org.opencdmp.model.builder.UsageLimitBuilder; import org.opencdmp.model.deleter.UsageLimitDeleter; import org.opencdmp.model.persist.UsageLimitPersist; +import org.opencdmp.query.UsageLimitQuery; +import org.opencdmp.service.accounting.AccountingService; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; @@ -40,6 +42,7 @@ import java.util.UUID; public class UsageLimitServiceImpl implements UsageLimitService { private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(UsageLimitServiceImpl.class)); + private static final Logger log = LoggerFactory.getLogger(UsageLimitServiceImpl.class); private final TenantEntityManager entityManager; @@ -55,7 +58,11 @@ public class UsageLimitServiceImpl implements UsageLimitService { private final MessageSource messageSource; - private final UserScope userScope; + private final QueryFactory queryFactory; + + private final TenantEntityManager tenantEntityManager; + + private final AccountingService accountingService; @Autowired @@ -67,7 +74,7 @@ public class UsageLimitServiceImpl implements UsageLimitService { ConventionService conventionService, ErrorThesaurusProperties errors, MessageSource messageSource, - UserScope userScope) { + QueryFactory queryFactory, TenantEntityManager tenantEntityManager, AccountingService accountingService) { this.entityManager = entityManager; this.authorizationService = authorizationService; this.deleterFactory = deleterFactory; @@ -75,11 +82,13 @@ public class UsageLimitServiceImpl implements UsageLimitService { this.conventionService = conventionService; this.errors = errors; this.messageSource = messageSource; - this.userScope = userScope; + this.queryFactory = queryFactory; + this.tenantEntityManager = tenantEntityManager; + this.accountingService = accountingService; } public UsageLimit persist(UsageLimitPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException { - logger.debug(new MapLogEntry("persisting data tag").And("model", model).And("fields", fields)); + logger.debug(new MapLogEntry("persisting data UsageLimit").And("model", model).And("fields", fields)); this.authorizationService.authorizeForce(Permission.EditUsageLimit); @@ -88,7 +97,7 @@ public class UsageLimitServiceImpl implements UsageLimitService { UsageLimitEntity data; if (isUpdate) { data = this.entityManager.find(UsageLimitEntity.class, model.getId()); - if (data == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{model.getId(), Tag.class.getSimpleName()}, LocaleContextHolder.getLocale())); + if (data == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{model.getId(), UsageLimit.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 UsageLimitEntity(); @@ -98,7 +107,7 @@ public class UsageLimitServiceImpl implements UsageLimitService { } data.setLabel(model.getLabel()); - data.setMetricValue(model.getMetricValue()); + data.setTargetMetric(model.getTargetMetric()); data.setValue(model.getValue()); data.setUpdatedAt(Instant.now()); if (isUpdate) @@ -118,8 +127,27 @@ public class UsageLimitServiceImpl implements UsageLimitService { this.deleterFactory.deleter(UsageLimitDeleter.class).deleteAndSaveByIds(List.of(id)); } - public void checkIncrease(UsageLimitMetricValue metric) { - //TODO + public void checkIncrease(UsageLimitTargetMetric metric) { + if (metric == null) throw new MyApplicationException("Target Metric not defined"); + + Integer currentValue = this.accountingService.getCurrentMetricValue(metric); + + try { + this.tenantEntityManager.loadExplicitTenantFilters(); + UsageLimitEntity usageLimitEntity = this.queryFactory.query(UsageLimitQuery.class).disableTracking().usageLimitTargetMetrics(metric).isActive(IsActive.Active).firstAs(new BaseFieldSet().ensure(UsageLimit._targetMetric).ensure(UsageLimit._value)); + if (usageLimitEntity != null && currentValue > usageLimitEntity.getValue()) throw new MyValidationException(this.errors.getUsageLimitException().getCode(), this.errors.getUsageLimitException().getMessage()); + + } catch (InvalidApplicationException e) { + log.error(e.getMessage(), e); + throw new MyApplicationException(e.getMessage()); + } finally { + try { + this.tenantEntityManager.reloadTenantFilters(); + } catch (InvalidApplicationException e) { + log.error(e.getMessage(), e); + throw new MyApplicationException(e.getMessage()); + } + } } } diff --git a/backend/web/src/main/resources/config/errors.yml b/backend/web/src/main/resources/config/errors.yml index 0e937d749..8f9f9b836 100644 --- a/backend/web/src/main/resources/config/errors.yml +++ b/backend/web/src/main/resources/config/errors.yml @@ -121,4 +121,7 @@ error-thesaurus: message: Request has expired maxDescriptionsExceeded: code: 144 - message: Max descriptions exceeded for this plan \ No newline at end of file + message: Max descriptions exceeded for this plan + usageLimitException: + code: 145 + message: Usage limit exception for this target metric \ No newline at end of file diff --git a/backend/web/src/main/resources/config/permissions.yml b/backend/web/src/main/resources/config/permissions.yml index 8615d20d4..631b1f164 100644 --- a/backend/web/src/main/resources/config/permissions.yml +++ b/backend/web/src/main/resources/config/permissions.yml @@ -69,6 +69,16 @@ permissions: clients: [ ] allowAnonymous: true allowAuthenticated: true + PublicClonePlan: + roles: [ ] + clients: [ ] + allowAnonymous: false + allowAuthenticated: true + PublicCloneDescription: + roles: [ ] + clients: [ ] + allowAnonymous: false + allowAuthenticated: true BrowsePublicStatistics: roles: [ ] clients: [ ] diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 3c3d55883..8f46be4f9 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -357,6 +357,19 @@ const appRoutes: Routes = [ title: 'GENERAL.TITLES.ENTITY-LOCKS' }, }, + { + path: 'usage-limits', + loadChildren: () => import('./ui/admin/usage-limit/usage-limit.module').then(m => m.UsageLimitModule), + data: { + authContext: { + permissions: [AppPermission.ViewUsageLimitPage] + }, + ...BreadcrumbService.generateRouteDataConfiguration({ + title: 'BREADCRUMBS.USAGE-LIMITS' + }), + title: 'GENERAL.TITLES.USAGE-LIMITS' + }, + }, { path: 'annotation-statuses', loadChildren: () => import('@annotation-service/ui/admin/status/status.module').then(m => m.StatusModule), diff --git a/frontend/src/app/core/common/enum/permission.enum.ts b/frontend/src/app/core/common/enum/permission.enum.ts index a57fa5b7e..d6516d5f1 100644 --- a/frontend/src/app/core/common/enum/permission.enum.ts +++ b/frontend/src/app/core/common/enum/permission.enum.ts @@ -13,6 +13,8 @@ export enum AppPermission { PublicBrowseDashboardStatistics = "PublicBrowseDashboardStatistics", PublicSendContactSupport = "PublicSendContactSupport", PublicBrowseReferenceType = "PublicBrowseReferenceType", + PublicClonePlan = "PublicClonePlan", + PublicCloneDescription = "PublicCloneDescription", //Elastic ManageElastic = "ManageElastic", //Queue Events @@ -201,6 +203,11 @@ export enum AppPermission { EditStatus = "EditStatus", DeleteStatus = "DeleteStatus", + //UsageLimit + BrowseUsageLimit = "BrowseUsageLimit", + EditUsageLimit = "EditUsageLimit", + DeleteUsageLimit = "DeleteUsageLimit", + // UI Pages ViewDescriptionTemplateTypePage = "ViewDescriptionTemplateTypePage", @@ -226,5 +233,6 @@ export enum AppPermission { ViewMineInAppNotificationPage = "ViewMineInAppNotificationPage", ViewTenantConfigurationPage = "ViewTenantConfigurationPage", ViewStatusPage = "ViewStatusPage", + ViewUsageLimitPage = "ViewUsageLimitPage", } diff --git a/frontend/src/app/core/common/enum/respone-error-code.ts b/frontend/src/app/core/common/enum/respone-error-code.ts index b2463cd73..c882b2da4 100644 --- a/frontend/src/app/core/common/enum/respone-error-code.ts +++ b/frontend/src/app/core/common/enum/respone-error-code.ts @@ -40,6 +40,7 @@ export enum ResponseErrorCode { InviteUserAlreadyConfirmed = 142, RequestHasExpired = 143, MaxDescriptionsExceeded = 144, + UsageLimitException = 145, // Notification & Annotation Errors InvalidApiKey = 200, @@ -154,6 +155,8 @@ export class ResponseErrorCodeHelper { return language.instant("GENERAL.BACKEND-ERRORS.REQUEST-HAS-EXPIRED"); case ResponseErrorCode.MaxDescriptionsExceeded: return language.instant("GENERAL.BACKEND-ERRORS.MAX-DESCRIPTION-EXCEEDED"); + case ResponseErrorCode.UsageLimitException: + return language.instant("GENERAL.BACKEND-ERRORS.USAGE-LIMIT-EXCEPTION"); default: return language.instant("GENERAL.SNACK-BAR.NOT-FOUND"); } diff --git a/frontend/src/app/core/common/enum/usage-limit-target-metric.ts b/frontend/src/app/core/common/enum/usage-limit-target-metric.ts new file mode 100644 index 000000000..7e03a2960 --- /dev/null +++ b/frontend/src/app/core/common/enum/usage-limit-target-metric.ts @@ -0,0 +1,10 @@ +export enum UsageLimitTargetMetric { + USER_COUNT = "user_count", + PLAN_COUNT = "plan_count", + BLUEPRINT_COUNT = "blueprint_count", + DESCRIPTION_COUNT = "description_count", + DESCRIPTION_TEMPLATE_COUNT = "description_template_count", + DESCRIPTION_TEMPLATE_TYPE_COUNT = "description_template_type_count", + PREFILLING_SOURCES_COUNT = "prefilling_sources_count", + REFERENCE_TYPE_COUNT = "reference_type_count" +} diff --git a/frontend/src/app/core/core-service.module.ts b/frontend/src/app/core/core-service.module.ts index a5dcccfe8..ad638df91 100644 --- a/frontend/src/app/core/core-service.module.ts +++ b/frontend/src/app/core/core-service.module.ts @@ -47,6 +47,7 @@ import { TenantConfigurationService } from './services/tenant-configuration/tena import { DefaultUserLocaleService } from './services/default-user-locale/default-user-locale.service'; import { TenantHandlingService } from './services/tenant/tenant-handling.service'; import { RouterUtilsService } from './services/router/router-utils.service'; +import { UsageLimitService } from './services/usage-limit/usage.service'; // // // This is shared module that provides all the services. Its imported only once on the AppModule. @@ -113,7 +114,8 @@ export class CoreServiceModule { TenantConfigurationService, StorageFileService, TenantHandlingService, - RouterUtilsService + RouterUtilsService, + UsageLimitService ], }; } diff --git a/frontend/src/app/core/model/usage-limit/usage-limit.ts b/frontend/src/app/core/model/usage-limit/usage-limit.ts new file mode 100644 index 000000000..029c1a71c --- /dev/null +++ b/frontend/src/app/core/model/usage-limit/usage-limit.ts @@ -0,0 +1,14 @@ +import { UsageLimitTargetMetric } from "@app/core/common/enum/usage-limit-target-metric"; +import { BaseEntity, BaseEntityPersist } from "@common/base/base-entity.model"; + +export interface UsageLimit extends BaseEntity { + label: string; + targetMetric: UsageLimitTargetMetric; + value: number; +} + +export interface UsageLimitPersist extends BaseEntityPersist { + label: string; + targetMetric: UsageLimitTargetMetric; + value: number; +} \ No newline at end of file diff --git a/frontend/src/app/core/query/usage-limit.lookup.ts b/frontend/src/app/core/query/usage-limit.lookup.ts new file mode 100644 index 000000000..b6bee939b --- /dev/null +++ b/frontend/src/app/core/query/usage-limit.lookup.ts @@ -0,0 +1,24 @@ +import { Lookup } from '@common/model/lookup'; +import { Guid } from '@common/types/guid'; +import { IsActive } from '../common/enum/is-active.enum'; +import { UsageLimitTargetMetric } from '../common/enum/usage-limit-target-metric'; + +export class UsageLimitLookup extends Lookup implements UsageLimitFilter { + ids: Guid[]; + usageLimitTargetMetrics: UsageLimitTargetMetric[]; + excludedIds: Guid[]; + like: string; + isActive: IsActive[]; + + constructor() { + super(); + } +} + +export interface UsageLimitFilter { + ids: Guid[]; + usageLimitTargetMetrics: UsageLimitTargetMetric[]; + excludedIds: Guid[]; + like: string; + isActive: IsActive[]; +} \ No newline at end of file diff --git a/frontend/src/app/core/services/usage-limit/usage.service.ts b/frontend/src/app/core/services/usage-limit/usage.service.ts new file mode 100644 index 000000000..b78f5a241 --- /dev/null +++ b/frontend/src/app/core/services/usage-limit/usage.service.ts @@ -0,0 +1,93 @@ +import { Injectable } from '@angular/core'; +import { IsActive } from '@app/core/common/enum/is-active.enum'; +import { UsageLimit, UsageLimitPersist } from '@app/core/model/usage-limit/usage-limit'; +import { UsageLimitLookup } from '@app/core/query/usage-limit.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 UsageLimitService { + + constructor(private http: BaseHttpV2Service, private configurationService: ConfigurationService, private filterService: FilterService) { + } + + private get apiBase(): string { return `${this.configurationService.server}usage-limit`; } + + query(q: UsageLimitLookup): 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: UsageLimitPersist): 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: UsageLimit) => item.label, + titleFn: (item: UsageLimit) => item.label, + valueAssign: (item: UsageLimit) => 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: UsageLimit) => item.label, + titleFn: (item: UsageLimit) => item.label, + valueAssign: (item: UsageLimit) => item.id, + }; + + private buildAutocompleteLookup(like?: string, excludedIds?: Guid[], ids?: Guid[]): UsageLimitLookup { + const lookup: UsageLimitLookup = new UsageLimitLookup(); + 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.label) + ] + }; + lookup.order = { items: [nameof(x => x.label)] }; + if (like) { lookup.like = this.filterService.transformLike(like); } + return lookup; + } +} \ No newline at end of file diff --git a/frontend/src/app/core/services/utilities/enum-utils.service.ts b/frontend/src/app/core/services/utilities/enum-utils.service.ts index bd9d0df24..1bd8b72ea 100644 --- a/frontend/src/app/core/services/utilities/enum-utils.service.ts +++ b/frontend/src/app/core/services/utilities/enum-utils.service.ts @@ -29,6 +29,7 @@ import { AppRole } from '../../common/enum/app-role'; import { PlanBlueprintExtraFieldDataType } from '../../common/enum/plan-blueprint-field-type'; import { PlanStatus } from '../../common/enum/plan-status'; import { ValidationType } from '../../common/enum/validation-type'; +import { UsageLimitTargetMetric } from '@app/core/common/enum/usage-limit-target-metric'; @Injectable() export class EnumUtils { @@ -303,4 +304,18 @@ export class EnumUtils { } } + public toUsageLimitTargetMetricString(value: UsageLimitTargetMetric): string { + switch (value) { + case UsageLimitTargetMetric.USER_COUNT: return this.language.instant('TYPES.USAGE-LIMIT-TARGET-METRIC.USER-COUNT'); + case UsageLimitTargetMetric.PLAN_COUNT: return this.language.instant('TYPES.USAGE-LIMIT-TARGET-METRIC.PLAN-COUNT'); + case UsageLimitTargetMetric.BLUEPRINT_COUNT: return this.language.instant('TYPES.USAGE-LIMIT-TARGET-METRIC.BLUEPRINT-COUNT'); + case UsageLimitTargetMetric.DESCRIPTION_COUNT: return this.language.instant('TYPES.USAGE-LIMIT-TARGET-METRIC.DESCRIPTION-COUNT'); + case UsageLimitTargetMetric.DESCRIPTION_TEMPLATE_COUNT: return this.language.instant('TYPES.USAGE-LIMIT-TARGET-METRIC.DESCRIPTION-TEMPLATE-COUNT'); + case UsageLimitTargetMetric.DESCRIPTION_TEMPLATE_TYPE_COUNT: return this.language.instant('TYPES.USAGE-LIMIT-TARGET-METRIC.DESCRIPTION-TEMPLATE-TYPE-COUNT'); + case UsageLimitTargetMetric.PREFILLING_SOURCES_COUNT: return this.language.instant('TYPES.USAGE-LIMIT-TARGET-METRIC.PREFILLING-SOURCES-COUNT'); + case UsageLimitTargetMetric.REFERENCE_TYPE_COUNT: return this.language.instant('TYPES.USAGE-LIMIT-TARGET-METRIC.REFERENCE-TYPE-COUNT'); + default: return ''; + } + } + } diff --git a/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.component.html b/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.component.html new file mode 100644 index 000000000..1ad115a2a --- /dev/null +++ b/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.component.html @@ -0,0 +1,61 @@ +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ + +
+
+ + {{'USAGE-LIMIT-EDITOR.FIELDS.LABEL' | translate}} + + {{formGroup.get('label').getError('backendError').message}} + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + +
+
+ + {{'USAGE-LIMIT-EDITOR.FIELDS.TARGET-METRIC' | translate}} + + {{enumUtils.toUsageLimitTargetMetricString(metric)}} + + {{formGroup.get('targetMetric').getError('backendError').message}} + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + +
+
+ + {{'USAGE-LIMIT-EDITOR.FIELDS.VALUE' | translate}} + + {{formGroup.get('value').getError('backendError').message}} + {{'GENERAL.VALIDATION.REQUIRED' | translate}} + +
+
+
+
+
+
+
+
diff --git a/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.component.scss b/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.component.scss new file mode 100644 index 000000000..ef2c86b91 --- /dev/null +++ b/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.component.scss @@ -0,0 +1,39 @@ +.usage-limit-editor { + + .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); +} + +::ng-deep .mat-checkbox-disabled.mat-checkbox-checked .mat-checkbox-background, .mat-checkbox-disabled.mat-checkbox-indeterminate .mat-checkbox-background { + background-color: #b0b0b0; +} + +.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; + } +} \ No newline at end of file diff --git a/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.component.ts b/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.component.ts new file mode 100644 index 000000000..cda056ec1 --- /dev/null +++ b/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.component.ts @@ -0,0 +1,183 @@ + +import { Component, OnInit } from '@angular/core'; +import { UntypedFormGroup } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { IsActive } from '@app/core/common/enum/is-active.enum'; +import { AppPermission } from '@app/core/common/enum/permission.enum'; +import { UsageLimit, UsageLimitPersist } from '@app/core/model/usage-limit/usage-limit'; +import { AuthService } from '@app/core/services/auth/auth.service'; +import { ConfigurationService } from '@app/core/services/configuration/configuration.service'; +import { LockService } from '@app/core/services/lock/lock.service'; +import { LoggingService } from '@app/core/services/logging/logging-service'; +import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; +import { UsageLimitService } from '@app/core/services/usage-limit/usage.service'; +import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; +import { QueryParamsService } from '@app/core/services/utilities/query-params.service'; +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 { UsageLimitEditorModel } from './usage-limit-editor.model'; +import { UsageLimitEditorResolver } from './usage-limit-editor.resolver'; +import { UsageLimitEditorService } from './usage-limit-editor.service'; +import { RouterUtilsService } from '@app/core/services/router/router-utils.service'; +import { Title } from '@angular/platform-browser'; +import { UsageLimitTargetMetric } from '@app/core/common/enum/usage-limit-target-metric'; + + +@Component({ + selector: 'app-usage-limit-editor-component', + templateUrl: 'usage-limit-editor.component.html', + styleUrls: ['./usage-limit-editor.component.scss'], + providers: [UsageLimitEditorService] +}) +export class UsageLimitEditorComponent extends BaseEditor implements OnInit { + + isNew = true; + isDeleted = false; + formGroup: UntypedFormGroup = null; + showInactiveDetails = false; + targetMetricEnum = this.enumUtils.getEnumValues(UsageLimitTargetMetric); + + protected get canDelete(): boolean { + return !this.isDeleted && !this.isNew && this.hasPermission(this.authService.permissionEnum.DeleteUsageLimit); + } + + protected get canSave(): boolean { + return !this.isDeleted && this.hasPermission(this.authService.permissionEnum.EditUsageLimit); + } + + protected get canFinalize(): boolean { + return !this.isDeleted && this.hasPermission(this.authService.permissionEnum.EditUsageLimit); + } + + + 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 route: ActivatedRoute, + protected queryParamsService: QueryParamsService, + protected lockService: LockService, + protected authService: AuthService, + protected configurationService: ConfigurationService, + // Rest dependencies. Inject any other needed deps here: + public enumUtils: EnumUtils, + private tenantService: UsageLimitService, + private logger: LoggingService, + private usageLimitEditorService: UsageLimitEditorService, + private titleService: Title, + protected routerUtils: RouterUtilsService, + ) { + const descriptionLabel: string = route.snapshot.data['entity']?.code; + if (descriptionLabel) { + titleService.setTitle(descriptionLabel); + } else { + titleService.setTitle('USAGE-LIMIT-EDITOR.TITLE-EDIT-USAGE-LIMIT'); + } + super(dialog, language, formService, router, uiNotificationService, httpErrorHandlingService, filterService, route, queryParamsService, lockService, authService, configurationService); + } + + ngOnInit(): void { + super.ngOnInit(); + } + + getItem(itemId: Guid, successFunction: (item: UsageLimit) => void) { + this.tenantService.getSingle(itemId, UsageLimitEditorResolver.lookupFields()) + .pipe(map(data => data as UsageLimit), takeUntil(this._destroyed)) + .subscribe( + data => successFunction(data), + error => this.onCallbackError(error) + ); + } + + prepareForm(data: UsageLimit) { + try { + this.editorModel = data ? new UsageLimitEditorModel().fromModel(data) : new UsageLimitEditorModel(); + + this.isDeleted = data ? data.isActive === IsActive.Inactive : false; + this.buildForm(); + } catch (error) { + this.logger.error('Could not parse UsageLimit 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.EditUsageLimit)); + this.usageLimitEditorService.setValidationErrorModel(this.editorModel.validationErrorModel); + } + + refreshData(): void { + this.getItem(this.editorModel.id, (data: UsageLimit) => this.prepareForm(data)); + } + + refreshOnNavigateToData(id?: Guid): void { + this.formGroup.markAsPristine(); + + this.router.navigate([this.routerUtils.generateUrl('/usage-limits')], { queryParams: { 'lookup': this.queryParamsService.serializeLookup(this.lookupParams), 'lv': ++this.lv }, replaceUrl: true, relativeTo: this.route }); + } + + persistEntity(onSuccess?: (response) => void): void { + const formData = this.formGroup.getRawValue() as UsageLimitPersist; + + this.tenantService.persist(formData) + .pipe(takeUntil(this._destroyed)).subscribe( + complete => onSuccess ? onSuccess(complete) : this.onCallbackSuccess(complete), + error => this.onCallbackError(error) + ); + } + + formSubmit(): void { + this.formService.removeAllBackEndErrors(this.formGroup); + 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.onCallbackDeleteSuccess(), + error => this.onCallbackError(error) + ); + } + }); + } + } + + clearErrorModel() { + this.editorModel.validationErrorModel.clear(); + this.formService.validateAllFormFields(this.formGroup); + } + +} diff --git a/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.model.ts b/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.model.ts new file mode 100644 index 000000000..a32d7e35c --- /dev/null +++ b/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.model.ts @@ -0,0 +1,54 @@ +import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms"; +import { UsageLimitTargetMetric } from "@app/core/common/enum/usage-limit-target-metric"; +import { UsageLimit, UsageLimitPersist } from "@app/core/model/usage-limit/usage-limit"; +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 UsageLimitEditorModel extends BaseEditorModel implements UsageLimitPersist { + label: string; + targetMetric: UsageLimitTargetMetric; + value: number; + permissions: string[]; + + public validationErrorModel: ValidationErrorModel = new ValidationErrorModel(); + protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder(); + + constructor() { super(); } + + public fromModel(item: UsageLimit): UsageLimitEditorModel { + if (item) { + super.fromModel(item); + this.label = item.label; + this.targetMetric = item.targetMetric; + this.value = item.value; + } + 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], + label: [{ value: this.label, disabled: disabled }, context.getValidation('label').validators], + targetMetric: [{ value: this.targetMetric, disabled: disabled }, context.getValidation('targetMetric').validators], + value: [{ value: this.value, disabled: disabled }, context.getValidation('value').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: 'label', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'label')] }); + baseValidationArray.push({ key: 'targetMetric', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'targetMetric')] }); + baseValidationArray.push({ key: 'value', validators: [Validators.required, BackendErrorValidator(this.validationErrorModel, 'value')] }); + baseValidationArray.push({ key: 'hash', validators: [] }); + + baseContext.validation = baseValidationArray; + return baseContext; + } +} diff --git a/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.resolver.ts b/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.resolver.ts new file mode 100644 index 000000000..f4e8a1567 --- /dev/null +++ b/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.resolver.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { UsageLimit } from '@app/core/model/usage-limit/usage-limit'; +import { UsageLimitService } from '@app/core/services/usage-limit/usage.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 UsageLimitEditorResolver extends BaseEditorResolver { + + constructor(private usageLimitService: UsageLimitService, private breadcrumbService: BreadcrumbService) { + super(); + } + + public static lookupFields(): string[] { + return [ + ...BaseEditorResolver.lookupFields(), + nameof(x => x.id), + nameof(x => x.label), + nameof(x => x.targetMetric), + nameof(x => x.value), + nameof(x => x.createdAt), + nameof(x => x.updatedAt), + nameof(x => x.hash), + nameof(x => x.isActive) + ] + } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + + const fields = [ + ...UsageLimitEditorResolver.lookupFields() + ]; + const id = route.paramMap.get('id'); + + if (id != null) { + return this.usageLimitService.getSingle(Guid.parse(id), fields).pipe(tap(x => this.breadcrumbService.addIdResolvedValue(x.id?.toString(), x.label)), takeUntil(this._destroyed)); + } + } +} diff --git a/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.service.ts b/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-editor.service.ts new file mode 100644 index 000000000..c98e95ad3 --- /dev/null +++ b/frontend/src/app/ui/admin/usage-limit/editor/usage-limit-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 UsageLimitEditorService { + private validationErrorModel: ValidationErrorModel; + + public setValidationErrorModel(validationErrorModel: ValidationErrorModel): void { + this.validationErrorModel = validationErrorModel; + } + + public getValidationErrorModel(): ValidationErrorModel { + return this.validationErrorModel; + } +} diff --git a/frontend/src/app/ui/admin/usage-limit/listing/filters/usage-limit-listing-filters.component.html b/frontend/src/app/ui/admin/usage-limit/listing/filters/usage-limit-listing-filters.component.html new file mode 100644 index 000000000..865a3107e --- /dev/null +++ b/frontend/src/app/ui/admin/usage-limit/listing/filters/usage-limit-listing-filters.component.html @@ -0,0 +1,59 @@ +
+ + + + + +
+
+
+

{{'USAGE-LIMIT-LISTING.FILTER.TITLE' | translate}}

+
+
+ +
+
+ +
+
+
+ + {{'USAGE-LIMIT-LISTING.FILTER.IS-ACTIVE' | translate}} + +
+
+
+ +
+
+ + {{'NOTIFICATION-SERVICE.NOTIFICATION-LISTING.FILTER.NOTIFICATION-TYPE' | translate}} + + {{enumUtils.toUsageLimitTargetMetricString(metric)}} + + +
+
+ +
+
+ +
+
+ +
+
+
+
+ + +
diff --git a/frontend/src/app/ui/admin/usage-limit/listing/filters/usage-limit-listing-filters.component.scss b/frontend/src/app/ui/admin/usage-limit/listing/filters/usage-limit-listing-filters.component.scss new file mode 100644 index 000000000..21216a40c --- /dev/null +++ b/frontend/src/app/ui/admin/usage-limit/listing/filters/usage-limit-listing-filters.component.scss @@ -0,0 +1,20 @@ +::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; +} + +::ng-deep .mdc-form-field { + label { + margin: 0; + } +} diff --git a/frontend/src/app/ui/admin/usage-limit/listing/filters/usage-limit-listing-filters.component.ts b/frontend/src/app/ui/admin/usage-limit/listing/filters/usage-limit-listing-filters.component.ts new file mode 100644 index 000000000..5bd277957 --- /dev/null +++ b/frontend/src/app/ui/admin/usage-limit/listing/filters/usage-limit-listing-filters.component.ts @@ -0,0 +1,101 @@ +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { IsActive } from '@app/core/common/enum/is-active.enum'; +import { UsageLimitTargetMetric } from '@app/core/common/enum/usage-limit-target-metric'; +import { UsageLimitFilter } from '@app/core/query/usage-limit.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-usage-limit-listing-filters', + templateUrl: './usage-limit-listing-filters.component.html', + styleUrls: ['./usage-limit-listing-filters.component.scss'] +}) +export class UsageLimitListingFiltersComponent extends BaseComponent implements OnInit, OnChanges { + + @Input() readonly filter: UsageLimitFilter; + @Output() filterChange = new EventEmitter(); + targetMetricEnumValues = this.enumUtils.getEnumValues(UsageLimitTargetMetric); + + + // * State + internalFilters: UsageLimitListingFilters = 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 UsageLimitFilter; + 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, usageLimitTargetMetrics} = this.internalFilters ?? {} + this.filterChange.emit({ + ...this.filter, + like, + isActive: isActive ? [IsActive.Active] : [IsActive.Inactive], + usageLimitTargetMetrics + }) + } + + + private _parseToInternalFilters(inputFilter: UsageLimitFilter): UsageLimitListingFilters { + if (!inputFilter) { + return this._getEmptyFilters(); + } + + let { excludedIds, ids, isActive, like, usageLimitTargetMetrics } = inputFilter; + + return { + isActive: (isActive ?? [])?.includes(IsActive.Active) || !isActive?.length, + like: like, + usageLimitTargetMetrics: usageLimitTargetMetrics + } + + } + + private _getEmptyFilters(): UsageLimitListingFilters { + return { + isActive: true, + like: null, + usageLimitTargetMetrics: null + } + } + + private _computeAppliedFilters(filters: UsageLimitListingFilters): number { + let count = 0; + if (filters?.isActive) { + count++ + } + return count; + } + + clearFilters() { + this.internalFilters = this._getEmptyFilters(); + } +} + +interface UsageLimitListingFilters { + isActive: boolean; + usageLimitTargetMetrics: UsageLimitTargetMetric[]; + like: string; +} diff --git a/frontend/src/app/ui/admin/usage-limit/listing/usage-limit-listing.component.html b/frontend/src/app/ui/admin/usage-limit/listing/usage-limit-listing.component.html new file mode 100644 index 000000000..61b9dd67e --- /dev/null +++ b/frontend/src/app/ui/admin/usage-limit/listing/usage-limit-listing.component.html @@ -0,0 +1,97 @@ +
+
+
+ +
+
+ + +
+
+ +
+
+ + + + + + + + +
+
+
+ + + + +
+
+ + {{item?.label | nullifyValue}} +
+
+ + + + {{'USAGE-LIMIT-LISTING.FIELDS.VALUE' | translate}}: + + {{item?.value | nullifyValue}} + + +
+
+ + + + {{'USAGE-LIMIT-LISTING.FIELDS.CREATED-AT' | translate}}: + + {{item?.createdAt | dateTimeFormatter : 'short' | nullifyValue}} + + +
+
+ + + {{'USAGE-LIMIT-LISTING.FIELDS.UPDATED-AT' | translate}}: + + {{item?.updatedAt | dateTimeFormatter : 'short' | nullifyValue}} + + + +
+
+
+ + +
+
+ + + + + +
+
+
\ No newline at end of file diff --git a/frontend/src/app/ui/admin/usage-limit/listing/usage-limit-listing.component.scss b/frontend/src/app/ui/admin/usage-limit/listing/usage-limit-listing.component.scss new file mode 100644 index 000000000..2a86ea230 --- /dev/null +++ b/frontend/src/app/ui/admin/usage-limit/listing/usage-limit-listing.component.scss @@ -0,0 +1,59 @@ +.usage-limit-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; + + .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/frontend/src/app/ui/admin/usage-limit/listing/usage-limit-listing.component.ts b/frontend/src/app/ui/admin/usage-limit/listing/usage-limit-listing.component.ts new file mode 100644 index 000000000..ba96d9ad3 --- /dev/null +++ b/frontend/src/app/ui/admin/usage-limit/listing/usage-limit-listing.component.ts @@ -0,0 +1,186 @@ +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 { UsageLimit } from '@app/core/model/usage-limit/usage-limit'; +import { UsageLimitLookup } from '@app/core/query/usage-limit.lookup'; +import { AuthService } from '@app/core/services/auth/auth.service'; +import { UsageLimitService } from '@app/core/services/usage-limit/usage.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 '@app/core/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 { IsActiveTypePipe } from '@common/formatting/pipes/is-active-type.pipe'; +import { RouterUtilsService } from '@app/core/services/router/router-utils.service'; +import { UsageLimitTargetMetricPipe } from '@common/formatting/pipes/usage-limits-target-metric.pipe'; +import { UsageLimitTargetMetric } from '@app/core/common/enum/usage-limit-target-metric'; + +@Component({ + templateUrl: './usage-limit-listing.component.html', + styleUrls: ['./usage-limit-listing.component.scss'] +}) +export class UsageLimitListingComponent extends BaseListingComponent implements OnInit { + publish = false; + userSettingsKey = { key: 'UsageLimitListingUserSettings' }; + 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.label), + nameof(x => x.targetMetric), + nameof(x => x.value), + nameof(x => x.updatedAt), + nameof(x => x.createdAt), + nameof(x => x.hash), + nameof(x => x.isActive) + ]; + + rowIdentity = x => x.id; + + constructor( + public routerUtils: RouterUtilsService, + protected router: Router, + protected route: ActivatedRoute, + protected uiNotificationService: UiNotificationService, + protected httpErrorHandlingService: HttpErrorHandlingService, + protected queryParamsService: QueryParamsService, + private usageLimitService: UsageLimitService, + 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(): UsageLimitLookup { + const lookup = new UsageLimitLookup(); + 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.label), + sortable: true, + languageName: 'USAGE-LIMIT-LISTING.FIELDS.LABEL' + }, { + prop: nameof(x => x.targetMetric), + sortable: true, + languageName: 'USAGE-LIMIT-LISTING.FIELDS.TARGET-METRIC', + pipe: this.pipeService.getPipe(UsageLimitTargetMetricPipe) + }, + { + prop: nameof(x => x.value), + sortable: true, + languageName: 'USAGE-LIMIT-LISTING.FIELDS.VALUE', + }, + { + prop: nameof(x => x.createdAt), + sortable: true, + languageName: 'USAGE-LIMIT-LISTING.FIELDS.CREATED-AT', + pipe: this.pipeService.getPipe(DataTableDateTimeFormatPipe).withFormat('short') + }, + { + prop: nameof(x => x.updatedAt), + sortable: true, + languageName: 'USAGE-LIMIT-LISTING.FIELDS.UPDATED-AT', + pipe: this.pipeService.getPipe(DataTableDateTimeFormatPipe).withFormat('short') + }, + { + prop: nameof(x => x.isActive), + sortable: true, + languageName: 'USAGE-LIMIT-LISTING.FIELDS.IS-ACTIVE', + pipe: this.pipeService.getPipe(IsActiveTypePipe) + }, + { + 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.usageLimitService.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.usageLimitService.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/frontend/src/app/ui/admin/usage-limit/usage-limit.module.ts b/frontend/src/app/ui/admin/usage-limit/usage-limit.module.ts new file mode 100644 index 000000000..5740f7832 --- /dev/null +++ b/frontend/src/app/ui/admin/usage-limit/usage-limit.module.ts @@ -0,0 +1,39 @@ +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 { UsageLimitRoutingModule } from './usage-limit.routing'; +import { UsageLimitEditorComponent } from './editor/usage-limit-editor.component'; +import { RichTextEditorModule } from '@app/library/rich-text-editor/rich-text-editor.module'; +import { UsageLimitListingComponent } from './listing/usage-limit-listing.component'; +import { UsageLimitListingFiltersComponent } from './listing/filters/usage-limit-listing-filters.component'; + +@NgModule({ + imports: [ + CommonUiModule, + CommonFormsModule, + ConfirmationDialogModule, + UsageLimitRoutingModule, + NgxDropzoneModule, + DragDropModule, + AutoCompleteModule, + HybridListingModule, + TextFilterModule, + UserSettingsModule, + CommonFormattingModule, + RichTextEditorModule + ], + declarations: [ + UsageLimitEditorComponent, + UsageLimitListingComponent, + UsageLimitListingFiltersComponent + ] +}) +export class UsageLimitModule { } diff --git a/frontend/src/app/ui/admin/usage-limit/usage-limit.routing.ts b/frontend/src/app/ui/admin/usage-limit/usage-limit.routing.ts new file mode 100644 index 000000000..e992cb99d --- /dev/null +++ b/frontend/src/app/ui/admin/usage-limit/usage-limit.routing.ts @@ -0,0 +1,54 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { UsageLimitEditorComponent } from './editor/usage-limit-editor.component'; +import { UsageLimitListingComponent } from './listing/usage-limit-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 { UsageLimitEditorResolver } from './editor/usage-limit-editor.resolver'; + +const routes: Routes = [ + { + path: '', + component: UsageLimitListingComponent, + canActivate: [AuthGuard] + }, + { + path: 'new', + canActivate: [AuthGuard], + component: UsageLimitEditorComponent, + canDeactivate: [PendingChangesGuard], + data: { + authContext: { + permissions: [AppPermission.EditUsageLimit] + }, + ...BreadcrumbService.generateRouteDataConfiguration({ + title: 'BREADCRUMBS.NEW-USAGE-LIMIT' + }) + } + }, + { + path: ':id', + canActivate: [AuthGuard], + component: UsageLimitEditorComponent, + canDeactivate: [PendingChangesGuard], + resolve: { + 'entity': UsageLimitEditorResolver + }, + data: { + authContext: { + permissions: [AppPermission.EditUsageLimit] + } + } + + }, + { path: '**', loadChildren: () => import('@common/modules/page-not-found/page-not-found.module').then(m => m.PageNotFoundModule) }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [UsageLimitEditorResolver] +}) +export class UsageLimitRoutingModule { } diff --git a/frontend/src/app/ui/description/overview/description-overview.component.html b/frontend/src/app/ui/description/overview/description-overview.component.html index df002994b..da244ce74 100644 --- a/frontend/src/app/ui/description/overview/description-overview.component.html +++ b/frontend/src/app/ui/description/overview/description-overview.component.html @@ -56,7 +56,7 @@ -
+
diff --git a/frontend/src/app/ui/description/overview/description-overview.component.ts b/frontend/src/app/ui/description/overview/description-overview.component.ts index bc8cd86ec..47e935803 100644 --- a/frontend/src/app/ui/description/overview/description-overview.component.ts +++ b/frontend/src/app/ui/description/overview/description-overview.component.ts @@ -68,6 +68,7 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni fileTransformerEntityTypeEnum = FileTransformerEntityType; canEdit = false; + canCopy = false; canDelete = false; canFinalize = false; canAnnotate = false; @@ -109,6 +110,7 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni this.canDelete = false; this.canEdit = false; + this.canCopy = false; this.canFinalize = false; this.canInvitePlanUsers = false; // Gets description data using parameter id @@ -134,6 +136,8 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni this.canEdit = (this.authService.hasPermission(AppPermission.EditDescription) || this.description.authorizationFlags?.some(x => x === AppPermission.EditDescription)) && this.description.belongsToCurrentTenant != false; + + this.canCopy = this.canEdit || (this.authService.hasPermission(AppPermission.PublicCloneDescription) && this.isPublicView); this.canAnnotate = (this.authService.hasPermission(AppPermission.AnnotateDescription) || this.description.authorizationFlags?.some(x => x === AppPermission.AnnotateDescription)) && this.description.belongsToCurrentTenant != false; @@ -162,6 +166,8 @@ export class DescriptionOverviewComponent extends BaseComponent implements OnIni this.descriptionService.getPublicSingle(publicId, this.lookupFields()) .pipe(takeUntil(this._destroyed)) .subscribe(data => { + this.canCopy = this.authService.hasPermission(AppPermission.PublicCloneDescription) && this.isPublicView; + this.breadcrumbService.addExcludedParam('public', true); this.breadcrumbService.addIdResolvedValue(data.id.toString(), data.label); diff --git a/frontend/src/app/ui/plan/listing/listing-item/plan-listing-item.component.ts b/frontend/src/app/ui/plan/listing/listing-item/plan-listing-item.component.ts index 18e3a31df..51ef6fbb0 100644 --- a/frontend/src/app/ui/plan/listing/listing-item/plan-listing-item.component.ts +++ b/frontend/src/app/ui/plan/listing/listing-item/plan-listing-item.component.ts @@ -248,7 +248,7 @@ export class PlanListingItemComponent extends BaseComponent implements OnInit { } canClonePlan(): boolean { - return this.plan.authorizationFlags?.some(x => x === AppPermission.ClonePlan) || this.authentication.hasPermission(AppPermission.ClonePlan); + return this.plan.authorizationFlags?.some(x => x === AppPermission.ClonePlan) || this.authentication.hasPermission(AppPermission.ClonePlan) || (this.authentication.hasPermission(AppPermission.PublicClonePlan) && this.isPublic); } canFinalizePlan(): boolean { diff --git a/frontend/src/app/ui/plan/overview/plan-overview.component.ts b/frontend/src/app/ui/plan/overview/plan-overview.component.ts index ce9a10200..5879385bd 100644 --- a/frontend/src/app/ui/plan/overview/plan-overview.component.ts +++ b/frontend/src/app/ui/plan/overview/plan-overview.component.ts @@ -255,7 +255,7 @@ export class PlanOverviewComponent extends BaseComponent implements OnInit { } canClonePlan(): boolean { - return (this.plan.authorizationFlags?.some(x => x === AppPermission.ClonePlan) || this.authentication.hasPermission(AppPermission.ClonePlan)); + return (this.plan.authorizationFlags?.some(x => x === AppPermission.ClonePlan) || this.authentication.hasPermission(AppPermission.ClonePlan) || (this.authentication.hasPermission(AppPermission.PublicClonePlan) && this.isPublicView)); } canFinalizePlan(): boolean { diff --git a/frontend/src/app/ui/sidebar/sidebar.component.ts b/frontend/src/app/ui/sidebar/sidebar.component.ts index 27c57ca32..c7f847c85 100644 --- a/frontend/src/app/ui/sidebar/sidebar.component.ts +++ b/frontend/src/app/ui/sidebar/sidebar.component.ts @@ -120,6 +120,7 @@ export class SidebarComponent implements OnInit { if (this.authentication.hasPermission(AppPermission.ViewReferencePage)) this.adminItems.routes.push({ path: '/references', title: 'SIDE-BAR.REFERENCES', icon: 'dataset_linked', routeType: RouteType.System }); if (this.authentication.hasPermission(AppPermission.ViewReferenceTypePage)) this.adminItems.routes.push({ path: '/reference-type', title: 'SIDE-BAR.REFERENCE-TYPES', icon: 'add_link', routeType: RouteType.System }); if (this.authentication.hasPermission(AppPermission.ViewPrefillingSourcePage)) this.adminItems.routes.push({ path: '/prefilling-sources', title: 'SIDE-BAR.PREFILLING-SOURCES', icon: 'quick_reference_all', routeType: RouteType.System }); + if (this.authentication.hasPermission(AppPermission.ViewUsageLimitPage)) this.adminItems.routes.push({ path: '/usage-limits', title: 'SIDE-BAR.USAGE-LIMITS', icon: 'quick_reference_all', routeType: RouteType.System }); if (this.authentication.hasPermission(AppPermission.ViewTenantPage)) this.adminItems.routes.push({ path: '/tenants', title: 'SIDE-BAR.TENANTS', icon: 'tenancy', routeType: RouteType.System }); if (this.authentication.hasPermission(AppPermission.ViewTenantConfigurationPage)) this.adminItems.routes.push({ path: '/tenant-configuration', title: 'SIDE-BAR.TENANT-CONFIGURATION', icon: 'settings', routeType: RouteType.System }); if (this.authentication.hasPermission(AppPermission.ViewUserPage)) this.adminItems.routes.push({ path: '/users', title: 'SIDE-BAR.USERS', icon: 'people', routeType: RouteType.System }); diff --git a/frontend/src/assets/i18n/baq.json b/frontend/src/assets/i18n/baq.json index 7457abc80..140a3542f 100644 --- a/frontend/src/assets/i18n/baq.json +++ b/frontend/src/assets/i18n/baq.json @@ -73,13 +73,14 @@ "PLAN-INACTIVE-USER": "This plan contains users that are not exist", "PLAN-MISSING-USER-CONTACT-INFO": "This plan contains users that don't have contact info", "DESCRIPTION-TEMPLATE-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this description template.", - "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", + "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", "REFERENCE-TYPE-CODE-EXISTS": "The reference type code you provided already exists. Please choose a different code.", "PREFILLING-SOURCE-CODE-EXISTS": "The prefilling source code you provided already exists. Please choose a different code.", "DUPLICATE-PLAN-USER": "You can't invite authors with same role and plan section more than once", "INVITE-USER-ALREADY-CONFIRMED": "Ιnvitation has already confirmed", "REQUEST-HAS-EXPIRED": "Request has expired", - "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template" + "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template", + "USAGE-LIMIT-EXCEPTION": "You have reached the available number of items for this entity " }, "FORM-VALIDATION-DISPLAY-DIALOG": { "WARNING": "Kontuz!", @@ -2123,6 +2124,16 @@ "DESCRIPTION": "Description", "PLAN-BLUEPRINT": "Plan Blueprint", "DESCRIPTION-TEMPLATE": "Description Template" + }, + "USAGE-LIMIT-TARGET-METRIC": { + "USER-COUNT": "Users", + "PLAN-COUNT": "Plans", + "BLUEPRINT-COUNT": "Plan Blueprints", + "DESCRIPTION-COUNT": "Descriptions", + "DESCRIPTION-TEMPLATE-COUNT": "Description Templates", + "DESCRIPTION-TEMPLATE-TYPE-COUNT": "Description Template Types", + "PREFILLING-SOURCES-COUNT": "Prefillings Sources", + "REFERENCE-TYPE-COUNT": "Reference Types" } }, "FOOTER": { diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json index a988bc3b3..25c1048dc 100644 --- a/frontend/src/assets/i18n/de.json +++ b/frontend/src/assets/i18n/de.json @@ -73,13 +73,14 @@ "PLAN-INACTIVE-USER": "This plan contains users that are not exist", "PLAN-MISSING-USER-CONTACT-INFO": "This plan contains users that don't have contact info", "DESCRIPTION-TEMPLATE-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this description template.", - "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", + "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", "REFERENCE-TYPE-CODE-EXISTS": "The reference type code you provided already exists. Please choose a different code.", "PREFILLING-SOURCE-CODE-EXISTS": "The prefilling source code you provided already exists. Please choose a different code.", "DUPLICATE-PLAN-USER": "You can't invite authors with same role and plan section more than once", "INVITE-USER-ALREADY-CONFIRMED": "Ιnvitation has already confirmed", "REQUEST-HAS-EXPIRED": "Request has expired", - "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template" + "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template", + "USAGE-LIMIT-EXCEPTION": "You have reached the available number of items for this entity " }, "FORM-VALIDATION-DISPLAY-DIALOG": { "WARNING": "Warnung!", @@ -2123,6 +2124,16 @@ "DESCRIPTION": "Description", "PLAN-BLUEPRINT": "Plan Blueprint", "DESCRIPTION-TEMPLATE": "Description Template" + }, + "USAGE-LIMIT-TARGET-METRIC": { + "USER-COUNT": "Users", + "PLAN-COUNT": "Plans", + "BLUEPRINT-COUNT": "Plan Blueprints", + "DESCRIPTION-COUNT": "Descriptions", + "DESCRIPTION-TEMPLATE-COUNT": "Description Templates", + "DESCRIPTION-TEMPLATE-TYPE-COUNT": "Description Template Types", + "PREFILLING-SOURCES-COUNT": "Prefillings Sources", + "REFERENCE-TYPE-COUNT": "Reference Types" } }, "FOOTER": { diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index b6f374e24..dc5b11a04 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -79,7 +79,8 @@ "DUPLICATE-PLAN-USER": "You can't invite authors with same role and plan section more than once", "INVITE-USER-ALREADY-CONFIRMED": "Ιnvitation has already confirmed", "REQUEST-HAS-EXPIRED": "Request has expired", - "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template" + "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template", + "USAGE-LIMIT-EXCEPTION": "You have reached the available number of items for this entity " }, "FORM-VALIDATION-DISPLAY-DIALOG": { "WARNING": "Warning!", @@ -2123,6 +2124,16 @@ "DESCRIPTION": "Description", "PLAN-BLUEPRINT": "Plan Blueprint", "DESCRIPTION-TEMPLATE": "Description Template" + }, + "USAGE-LIMIT-TARGET-METRIC": { + "USER-COUNT": "Users", + "PLAN-COUNT": "Plans", + "BLUEPRINT-COUNT": "Plan Blueprints", + "DESCRIPTION-COUNT": "Descriptions", + "DESCRIPTION-TEMPLATE-COUNT": "Description Templates", + "DESCRIPTION-TEMPLATE-TYPE-COUNT": "Description Template Types", + "PREFILLING-SOURCES-COUNT": "Prefillings Sources", + "REFERENCE-TYPE-COUNT": "Reference Types" } }, "FOOTER": { diff --git a/frontend/src/assets/i18n/es.json b/frontend/src/assets/i18n/es.json index 94fc667da..f85192a60 100644 --- a/frontend/src/assets/i18n/es.json +++ b/frontend/src/assets/i18n/es.json @@ -73,13 +73,14 @@ "PLAN-INACTIVE-USER": "This plan contains users that are not exist", "PLAN-MISSING-USER-CONTACT-INFO": "This plan contains users that don't have contact info", "DESCRIPTION-TEMPLATE-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this description template.", - "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", + "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", "REFERENCE-TYPE-CODE-EXISTS": "The reference type code you provided already exists. Please choose a different code.", "PREFILLING-SOURCE-CODE-EXISTS": "The prefilling source code you provided already exists. Please choose a different code.", "DUPLICATE-PLAN-USER": "You can't invite authors with same role and plan section more than once", "INVITE-USER-ALREADY-CONFIRMED": "Ιnvitation has already confirmed", "REQUEST-HAS-EXPIRED": "Request has expired", - "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template" + "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template", + "USAGE-LIMIT-EXCEPTION": "You have reached the available number of items for this entity " }, "FORM-VALIDATION-DISPLAY-DIALOG": { "WARNING": "Atención!", @@ -2123,6 +2124,16 @@ "DESCRIPTION": "Description", "PLAN-BLUEPRINT": "Plan Blueprint", "DESCRIPTION-TEMPLATE": "Description Template" + }, + "USAGE-LIMIT-TARGET-METRIC": { + "USER-COUNT": "Users", + "PLAN-COUNT": "Plans", + "BLUEPRINT-COUNT": "Plan Blueprints", + "DESCRIPTION-COUNT": "Descriptions", + "DESCRIPTION-TEMPLATE-COUNT": "Description Templates", + "DESCRIPTION-TEMPLATE-TYPE-COUNT": "Description Template Types", + "PREFILLING-SOURCES-COUNT": "Prefillings Sources", + "REFERENCE-TYPE-COUNT": "Reference Types" } }, "FOOTER": { diff --git a/frontend/src/assets/i18n/gr.json b/frontend/src/assets/i18n/gr.json index 748f2b041..63a6a13ca 100644 --- a/frontend/src/assets/i18n/gr.json +++ b/frontend/src/assets/i18n/gr.json @@ -73,13 +73,14 @@ "PLAN-INACTIVE-USER": "This plan contains users that are not exist", "PLAN-MISSING-USER-CONTACT-INFO": "This plan contains users that don't have contact info", "DESCRIPTION-TEMPLATE-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this description template.", - "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", + "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", "REFERENCE-TYPE-CODE-EXISTS": "The reference type code you provided already exists. Please choose a different code.", "PREFILLING-SOURCE-CODE-EXISTS": "The prefilling source code you provided already exists. Please choose a different code.", "DUPLICATE-PLAN-USER": "You can't invite authors with same role and plan section more than once", "INVITE-USER-ALREADY-CONFIRMED": "Ιnvitation has already confirmed", "REQUEST-HAS-EXPIRED": "Request has expired", - "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template" + "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template", + "USAGE-LIMIT-EXCEPTION": "You have reached the available number of items for this entity " }, "FORM-VALIDATION-DISPLAY-DIALOG": { "WARNING": "Προσοχή!", @@ -2123,6 +2124,16 @@ "DESCRIPTION": "Description", "PLAN-BLUEPRINT": "Plan Blueprint", "DESCRIPTION-TEMPLATE": "Description Template" + }, + "USAGE-LIMIT-TARGET-METRIC": { + "USER-COUNT": "Users", + "PLAN-COUNT": "Plans", + "BLUEPRINT-COUNT": "Plan Blueprints", + "DESCRIPTION-COUNT": "Descriptions", + "DESCRIPTION-TEMPLATE-COUNT": "Description Templates", + "DESCRIPTION-TEMPLATE-TYPE-COUNT": "Description Template Types", + "PREFILLING-SOURCES-COUNT": "Prefillings Sources", + "REFERENCE-TYPE-COUNT": "Reference Types" } }, "FOOTER": { diff --git a/frontend/src/assets/i18n/hr.json b/frontend/src/assets/i18n/hr.json index 5f461df23..3832c8dca 100644 --- a/frontend/src/assets/i18n/hr.json +++ b/frontend/src/assets/i18n/hr.json @@ -73,13 +73,14 @@ "PLAN-INACTIVE-USER": "This plan contains users that are not exist", "PLAN-MISSING-USER-CONTACT-INFO": "This plan contains users that don't have contact info", "DESCRIPTION-TEMPLATE-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this description template.", - "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", + "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", "REFERENCE-TYPE-CODE-EXISTS": "The reference type code you provided already exists. Please choose a different code.", "PREFILLING-SOURCE-CODE-EXISTS": "The prefilling source code you provided already exists. Please choose a different code.", "DUPLICATE-PLAN-USER": "You can't invite authors with same role and plan section more than once", "INVITE-USER-ALREADY-CONFIRMED": "Ιnvitation has already confirmed", "REQUEST-HAS-EXPIRED": "Request has expired", - "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template" + "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template", + "USAGE-LIMIT-EXCEPTION": "You have reached the available number of items for this entity " }, "FORM-VALIDATION-DISPLAY-DIALOG": { "WARNING": "Oprez!", @@ -2123,6 +2124,16 @@ "DESCRIPTION": "Description", "PLAN-BLUEPRINT": "Plan Blueprint", "DESCRIPTION-TEMPLATE": "Description Template" + }, + "USAGE-LIMIT-TARGET-METRIC": { + "USER-COUNT": "Users", + "PLAN-COUNT": "Plans", + "BLUEPRINT-COUNT": "Plan Blueprints", + "DESCRIPTION-COUNT": "Descriptions", + "DESCRIPTION-TEMPLATE-COUNT": "Description Templates", + "DESCRIPTION-TEMPLATE-TYPE-COUNT": "Description Template Types", + "PREFILLING-SOURCES-COUNT": "Prefillings Sources", + "REFERENCE-TYPE-COUNT": "Reference Types" } }, "FOOTER": { diff --git a/frontend/src/assets/i18n/pl.json b/frontend/src/assets/i18n/pl.json index 4c6ab979a..cbe11c61a 100644 --- a/frontend/src/assets/i18n/pl.json +++ b/frontend/src/assets/i18n/pl.json @@ -73,13 +73,14 @@ "PLAN-INACTIVE-USER": "This plan contains users that are not exist", "PLAN-MISSING-USER-CONTACT-INFO": "This plan contains users that don't have contact info", "DESCRIPTION-TEMPLATE-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this description template.", - "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", + "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", "REFERENCE-TYPE-CODE-EXISTS": "The reference type code you provided already exists. Please choose a different code.", "PREFILLING-SOURCE-CODE-EXISTS": "The prefilling source code you provided already exists. Please choose a different code.", "DUPLICATE-PLAN-USER": "You can't invite authors with same role and plan section more than once", "INVITE-USER-ALREADY-CONFIRMED": "Ιnvitation has already confirmed", "REQUEST-HAS-EXPIRED": "Request has expired", - "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template" + "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template", + "USAGE-LIMIT-EXCEPTION": "You have reached the available number of items for this entity " }, "FORM-VALIDATION-DISPLAY-DIALOG": { "WARNING": "Ostrzeżenie!", @@ -2123,6 +2124,16 @@ "DESCRIPTION": "Description", "PLAN-BLUEPRINT": "Plan Blueprint", "DESCRIPTION-TEMPLATE": "Description Template" + }, + "USAGE-LIMIT-TARGET-METRIC": { + "USER-COUNT": "Users", + "PLAN-COUNT": "Plans", + "BLUEPRINT-COUNT": "Plan Blueprints", + "DESCRIPTION-COUNT": "Descriptions", + "DESCRIPTION-TEMPLATE-COUNT": "Description Templates", + "DESCRIPTION-TEMPLATE-TYPE-COUNT": "Description Template Types", + "PREFILLING-SOURCES-COUNT": "Prefillings Sources", + "REFERENCE-TYPE-COUNT": "Reference Types" } }, "FOOTER": { diff --git a/frontend/src/assets/i18n/pt.json b/frontend/src/assets/i18n/pt.json index a5d1fe18e..9a347827b 100644 --- a/frontend/src/assets/i18n/pt.json +++ b/frontend/src/assets/i18n/pt.json @@ -73,13 +73,14 @@ "PLAN-INACTIVE-USER": "This plan contains users that are not exist", "PLAN-MISSING-USER-CONTACT-INFO": "This plan contains users that don't have contact info", "DESCRIPTION-TEMPLATE-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this description template.", - "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", + "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", "REFERENCE-TYPE-CODE-EXISTS": "The reference type code you provided already exists. Please choose a different code.", "PREFILLING-SOURCE-CODE-EXISTS": "The prefilling source code you provided already exists. Please choose a different code.", "DUPLICATE-PLAN-USER": "You can't invite authors with same role and plan section more than once", "INVITE-USER-ALREADY-CONFIRMED": "Ιnvitation has already confirmed", "REQUEST-HAS-EXPIRED": "Request has expired", - "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template" + "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template", + "USAGE-LIMIT-EXCEPTION": "You have reached the available number of items for this entity " }, "FORM-VALIDATION-DISPLAY-DIALOG": { "WARNING": "Atenção!", @@ -2123,6 +2124,16 @@ "DESCRIPTION": "Description", "PLAN-BLUEPRINT": "Plan Blueprint", "DESCRIPTION-TEMPLATE": "Description Template" + }, + "USAGE-LIMIT-TARGET-METRIC": { + "USER-COUNT": "Users", + "PLAN-COUNT": "Plans", + "BLUEPRINT-COUNT": "Plan Blueprints", + "DESCRIPTION-COUNT": "Descriptions", + "DESCRIPTION-TEMPLATE-COUNT": "Description Templates", + "DESCRIPTION-TEMPLATE-TYPE-COUNT": "Description Template Types", + "PREFILLING-SOURCES-COUNT": "Prefillings Sources", + "REFERENCE-TYPE-COUNT": "Reference Types" } }, "FOOTER": { diff --git a/frontend/src/assets/i18n/sk.json b/frontend/src/assets/i18n/sk.json index 7b2aeee89..6f33012b4 100644 --- a/frontend/src/assets/i18n/sk.json +++ b/frontend/src/assets/i18n/sk.json @@ -73,13 +73,14 @@ "PLAN-INACTIVE-USER": "This plan contains users that are not exist", "PLAN-MISSING-USER-CONTACT-INFO": "This plan contains users that don't have contact info", "DESCRIPTION-TEMPLATE-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this description template.", - "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", + "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", "REFERENCE-TYPE-CODE-EXISTS": "The reference type code you provided already exists. Please choose a different code.", "PREFILLING-SOURCE-CODE-EXISTS": "The prefilling source code you provided already exists. Please choose a different code.", "DUPLICATE-PLAN-USER": "You can't invite authors with same role and plan section more than once", "INVITE-USER-ALREADY-CONFIRMED": "Ιnvitation has already confirmed", "REQUEST-HAS-EXPIRED": "Request has expired", - "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template" + "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template", + "USAGE-LIMIT-EXCEPTION": "You have reached the available number of items for this entity " }, "FORM-VALIDATION-DISPLAY-DIALOG": { "WARNING": "Upozornenie!", @@ -2123,6 +2124,16 @@ "DESCRIPTION": "Description", "PLAN-BLUEPRINT": "Plan Blueprint", "DESCRIPTION-TEMPLATE": "Description Template" + }, + "USAGE-LIMIT-TARGET-METRIC": { + "USER-COUNT": "Users", + "PLAN-COUNT": "Plans", + "BLUEPRINT-COUNT": "Plan Blueprints", + "DESCRIPTION-COUNT": "Descriptions", + "DESCRIPTION-TEMPLATE-COUNT": "Description Templates", + "DESCRIPTION-TEMPLATE-TYPE-COUNT": "Description Template Types", + "PREFILLING-SOURCES-COUNT": "Prefillings Sources", + "REFERENCE-TYPE-COUNT": "Reference Types" } }, "FOOTER": { diff --git a/frontend/src/assets/i18n/sr.json b/frontend/src/assets/i18n/sr.json index 3b6bc64f7..3d8a295fd 100644 --- a/frontend/src/assets/i18n/sr.json +++ b/frontend/src/assets/i18n/sr.json @@ -73,13 +73,14 @@ "PLAN-INACTIVE-USER": "This plan contains users that are not exist", "PLAN-MISSING-USER-CONTACT-INFO": "This plan contains users that don't have contact info", "DESCRIPTION-TEMPLATE-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this description template.", - "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", + "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", "REFERENCE-TYPE-CODE-EXISTS": "The reference type code you provided already exists. Please choose a different code.", "PREFILLING-SOURCE-CODE-EXISTS": "The prefilling source code you provided already exists. Please choose a different code.", "DUPLICATE-PLAN-USER": "You can't invite authors with same role and plan section more than once", "INVITE-USER-ALREADY-CONFIRMED": "Ιnvitation has already confirmed", "REQUEST-HAS-EXPIRED": "Request has expired", - "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template" + "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template", + "USAGE-LIMIT-EXCEPTION": "You have reached the available number of items for this entity " }, "FORM-VALIDATION-DISPLAY-DIALOG": { "WARNING": "Oprez!", @@ -2123,6 +2124,16 @@ "DESCRIPTION": "Description", "PLAN-BLUEPRINT": "Plan Blueprint", "DESCRIPTION-TEMPLATE": "Description Template" + }, + "USAGE-LIMIT-TARGET-METRIC": { + "USER-COUNT": "Users", + "PLAN-COUNT": "Plans", + "BLUEPRINT-COUNT": "Plan Blueprints", + "DESCRIPTION-COUNT": "Descriptions", + "DESCRIPTION-TEMPLATE-COUNT": "Description Templates", + "DESCRIPTION-TEMPLATE-TYPE-COUNT": "Description Template Types", + "PREFILLING-SOURCES-COUNT": "Prefillings Sources", + "REFERENCE-TYPE-COUNT": "Reference Types" } }, "FOOTER": { diff --git a/frontend/src/assets/i18n/tr.json b/frontend/src/assets/i18n/tr.json index bc8401c4c..8c6948702 100644 --- a/frontend/src/assets/i18n/tr.json +++ b/frontend/src/assets/i18n/tr.json @@ -73,13 +73,14 @@ "PLAN-INACTIVE-USER": "This plan contains users that are not exist", "PLAN-MISSING-USER-CONTACT-INFO": "This plan contains users that don't have contact info", "DESCRIPTION-TEMPLATE-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this description template.", - "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", + "PLAN-BLUEPRINT-NEW-VERSION-ALREADY-CREATED-DRAFT": "You have already created a new draft version for this blueprint.", "REFERENCE-TYPE-CODE-EXISTS": "The reference type code you provided already exists. Please choose a different code.", "PREFILLING-SOURCE-CODE-EXISTS": "The prefilling source code you provided already exists. Please choose a different code.", "DUPLICATE-PLAN-USER": "You can't invite authors with same role and plan section more than once", "INVITE-USER-ALREADY-CONFIRMED": "Ιnvitation has already confirmed", "REQUEST-HAS-EXPIRED": "Request has expired", - "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template" + "MAX-DESCRIPTION-EXCEEDED": "This plan has reached the maximun descriptions for this description template", + "USAGE-LIMIT-EXCEPTION": "You have reached the available number of items for this entity " }, "FORM-VALIDATION-DISPLAY-DIALOG": { "WARNING": "Uyarı!", @@ -2123,6 +2124,16 @@ "DESCRIPTION": "Description", "PLAN-BLUEPRINT": "Plan Blueprint", "DESCRIPTION-TEMPLATE": "Description Template" + }, + "USAGE-LIMIT-TARGET-METRIC": { + "USER-COUNT": "Users", + "PLAN-COUNT": "Plans", + "BLUEPRINT-COUNT": "Plan Blueprints", + "DESCRIPTION-COUNT": "Descriptions", + "DESCRIPTION-TEMPLATE-COUNT": "Description Templates", + "DESCRIPTION-TEMPLATE-TYPE-COUNT": "Description Template Types", + "PREFILLING-SOURCES-COUNT": "Prefillings Sources", + "REFERENCE-TYPE-COUNT": "Reference Types" } }, "FOOTER": { diff --git a/frontend/src/common/formatting/common-formatting.module.ts b/frontend/src/common/formatting/common-formatting.module.ts index c4f720963..a55e0c7c8 100644 --- a/frontend/src/common/formatting/common-formatting.module.ts +++ b/frontend/src/common/formatting/common-formatting.module.ts @@ -12,6 +12,7 @@ import { NotificationTrackingStatePipe } from './pipes/notification-tracking-sta import { NotificationTypePipe } from './pipes/notification-type.pipe'; import { ReferenceSourceTypePipe } from './pipes/reference-source-type.pipe'; import { LockTargetTypePipe } from './pipes/lock-target-type.pipe'; +import { UsageLimitTargetMetricPipe } from './pipes/usage-limits-target-metric.pipe'; // // @@ -33,7 +34,8 @@ import { LockTargetTypePipe } from './pipes/lock-target-type.pipe'; NotificationNotifyStatePipe, NotificationTrackingProcessPipe, NotificationTrackingStatePipe, - LockTargetTypePipe + LockTargetTypePipe, + UsageLimitTargetMetricPipe ], exports: [ LowercaseFirstLetterPipe, @@ -49,7 +51,8 @@ import { LockTargetTypePipe } from './pipes/lock-target-type.pipe'; NotificationNotifyStatePipe, NotificationTrackingProcessPipe, NotificationTrackingStatePipe, - LockTargetTypePipe + LockTargetTypePipe, + UsageLimitTargetMetricPipe ], providers: [ LowercaseFirstLetterPipe, @@ -65,7 +68,8 @@ import { LockTargetTypePipe } from './pipes/lock-target-type.pipe'; NotificationNotifyStatePipe, NotificationTrackingProcessPipe, NotificationTrackingStatePipe, - LockTargetTypePipe + LockTargetTypePipe, + UsageLimitTargetMetricPipe ] }) export class CommonFormattingModule { } diff --git a/frontend/src/common/formatting/pipes/usage-limits-target-metric.pipe.ts b/frontend/src/common/formatting/pipes/usage-limits-target-metric.pipe.ts new file mode 100644 index 000000000..e0ca52a6c --- /dev/null +++ b/frontend/src/common/formatting/pipes/usage-limits-target-metric.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; + +@Pipe({ name: 'UsageLimitTargetMetricFormat' }) +export class UsageLimitTargetMetricPipe implements PipeTransform { + constructor(private enumUtils: EnumUtils) { } + + public transform(value): any { + return this.enumUtils.toUsageLimitTargetMetricString(value); + } +}