Browse Source

Merge pull request #14551 from thingsboard/feature/entity-limit-request

Handling of the entities limit exceeded violation
pull/14557/head
Igor Kulikov 6 months ago
committed by GitHub
parent
commit
716bb9df05
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  2. 39
      application/src/main/java/org/thingsboard/server/controller/NotificationController.java
  3. 50
      application/src/main/java/org/thingsboard/server/exception/ThingsboardEntitiesLimitExceededResponse.java
  4. 3
      application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java
  5. 23
      application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java
  6. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  7. 4
      application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
  8. 1
      common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java
  9. 1
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
  10. 50
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntitiesLimitIncreaseRequestNotificationInfo.java
  11. 8
      dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitExceededException.java
  12. 4
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java
  13. 61
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
  14. 5
      dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java
  15. 5
      ui-ngx/src/app/core/http/notification.service.ts
  16. 4
      ui-ngx/src/app/core/interceptors/global-http-interceptor.ts
  17. 15
      ui-ngx/src/app/core/services/dialog.service.ts
  18. 1
      ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts
  19. 40
      ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.html
  20. 97
      ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.scss
  21. 100
      ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.ts
  22. 2
      ui-ngx/src/app/shared/components/notification/notification.component.ts
  23. 2
      ui-ngx/src/app/shared/models/constants.ts
  24. 8
      ui-ngx/src/app/shared/models/notification.models.ts
  25. 3
      ui-ngx/src/app/shared/shared.module.ts
  26. 20
      ui-ngx/src/assets/entity-limit-reached.svg
  27. 46
      ui-ngx/src/assets/help/en_US/notification/entities_limit_increase_request.md
  28. 14
      ui-ngx/src/assets/locale/locale.constant-en_US.json

3
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -151,6 +151,7 @@ import org.thingsboard.server.dao.domain.DomainService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.EntitiesLimitExceededException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.job.JobService;
import org.thingsboard.server.dao.mobile.MobileAppBundleService;
@ -452,6 +453,8 @@ public abstract class BaseController {
}
if (exception instanceof ThingsboardException) {
return (ThingsboardException) exception;
} else if (exception instanceof EntitiesLimitExceededException) {
return new ThingsboardException(exception, ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED);
} else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException
|| exception instanceof DataValidationException || cause instanceof IncorrectParameterException) {
return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS);

39
application/src/main/java/org/thingsboard/server/controller/NotificationController.java

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -35,6 +36,8 @@ import org.thingsboard.rule.engine.api.NotificationCenter;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.NotificationId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.NotificationTargetId;
@ -44,6 +47,9 @@ import org.thingsboard.server.common.data.notification.NotificationDeliveryMetho
import org.thingsboard.server.common.data.notification.NotificationRequest;
import org.thingsboard.server.common.data.notification.NotificationRequestInfo;
import org.thingsboard.server.common.data.notification.NotificationRequestPreview;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.EntitiesLimitIncreaseRequestNotificationInfo;
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.settings.UserNotificationSettings;
import org.thingsboard.server.common.data.notification.targets.MicrosoftTeamsNotificationTargetConfig;
@ -51,6 +57,7 @@ import org.thingsboard.server.common.data.notification.targets.NotificationRecip
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.NotificationTargetType;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
@ -69,6 +76,7 @@ import org.thingsboard.server.service.notification.NotificationProcessingContext
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import java.util.Comparator;
import java.util.HashMap;
@ -76,6 +84,7 @@ import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@ -90,6 +99,7 @@ import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DE
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.service.security.permission.Resource.NOTIFICATION;
@RestController
@ -105,6 +115,7 @@ public class NotificationController extends BaseController {
private final NotificationTargetService notificationTargetService;
private final NotificationCenter notificationCenter;
private final NotificationSettingsService notificationSettingsService;
private final SystemSecurityService systemSecurityService;
@ApiOperation(value = "Get notifications (getNotifications)",
notes = "Returns the page of notifications for current user." + NEW_LINE +
@ -282,6 +293,34 @@ public class NotificationController extends BaseController {
return doSaveAndLog(EntityType.NOTIFICATION_REQUEST, notificationRequest, (tenantId, request) -> notificationCenter.processNotificationRequest(tenantId, request, null));
}
@ApiOperation(value = "Send entity limit increase request notification to System administrators (sendEntitiesLimitIncreaseRequest)",
notes = "Send entity limit increase request notification by Tenant Administrator to System administrators." +
TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/notification/entitiesLimitIncreaseRequest/{entityType}")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
public void sendEntitiesLimitIncreaseRequest(@Parameter(description = "Entity type", required = true, schema = @Schema(allowableValues = {"DEVICE", "ASSET", "CUSTOMER", "USER", "DASHBOARD", "RULE_CHAIN", "EDGE"}))
@PathVariable("entityType") String strEntityType,
@AuthenticationPrincipal SecurityUser user,
HttpServletRequest request) throws Exception {
EntityType entityType = checkEnumParameter("entityType", strEntityType, EntityType::valueOf);
Optional<NotificationTarget> sysAdmins = notificationTargetService.findNotificationTargetsByTenantIdAndUsersFilterType(TenantId.SYS_TENANT_ID, UsersFilterType.SYSTEM_ADMINISTRATORS)
.stream().findFirst();
if (sysAdmins.isPresent()) {
NotificationTargetId notificationTargetId = sysAdmins.get().getId();
String baseUrl = systemSecurityService.getBaseUrl(TenantId.SYS_TENANT_ID, new CustomerId(EntityId.NULL_UUID), request);
NotificationInfo info = EntitiesLimitIncreaseRequestNotificationInfo.builder()
.entityType(entityType)
.userEmail(user.getEmail())
.increaseLimitActionLabel("Set new limit")
.increaseLimitLink("/tenantProfiles/"+tenantService.findTenantById(user.getTenantId()).getTenantProfileId().toString())
.baseUrl(baseUrl)
.build();
notificationCenter.sendSystemNotification(TenantId.SYS_TENANT_ID, notificationTargetId, NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST, info);
} else {
throw new IllegalArgumentException("Notification target for 'System administrators' not found");
}
}
@ApiOperation(value = "Get notification request preview (getNotificationRequestPreview)",
notes = "Returns preview for notification request." + NEW_LINE +
"`processedTemplates` shows how the notifications for each delivery method will look like " +

50
application/src/main/java/org/thingsboard/server/exception/ThingsboardEntitiesLimitExceededResponse.java

@ -0,0 +1,50 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.exception;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.http.HttpStatus;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@Schema
public class ThingsboardEntitiesLimitExceededResponse extends ThingsboardErrorResponse {
private final EntityType entityType;
private final Long limit;
protected ThingsboardEntitiesLimitExceededResponse(String message, EntityType entityType, Long limit) {
super(message, ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED, HttpStatus.FORBIDDEN);
this.entityType = entityType;
this.limit = limit;
}
public static ThingsboardEntitiesLimitExceededResponse of(final String message, final EntityType entityType, final Long limit) {
return new ThingsboardEntitiesLimitExceededResponse(message, entityType, limit);
}
@Schema(description = "Entity type", accessMode = Schema.AccessMode.READ_ONLY)
public EntityType getEntityType() {
return entityType;
}
@Schema(description = "Limit", accessMode = Schema.AccessMode.READ_ONLY)
public Long getLimit() {
return limit;
}
}

3
application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java

@ -64,7 +64,8 @@ public class ThingsboardErrorResponse {
"\n\n* `32` - Item not found (HTTP: 404 - Not Found)" +
"\n\n* `33` - Too many requests (HTTP: 429 - Too Many Requests)" +
"\n\n* `34` - Too many updates (Too many updates over Websocket session)" +
"\n\n* `40` - Subscription violation (HTTP: 403 - Forbidden)",
"\n\n* `40` - Subscription violation (HTTP: 403 - Forbidden)" +
"\n\n* `41` - Entities limit exceeded (HTTP: 403 - Forbidden)",
example = "10", type = "integer",
accessMode = Schema.AccessMode.READ_ONLY)
public ThingsboardErrorCode getErrorCode() {

23
application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java

@ -47,10 +47,12 @@ import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.util.WebUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.dao.exception.EntitiesLimitExceededException;
import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
@ -95,6 +97,7 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
errorCodeToStatusMap.put(ThingsboardErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS);
errorCodeToStatusMap.put(ThingsboardErrorCode.TOO_MANY_UPDATES, HttpStatus.TOO_MANY_REQUESTS);
errorCodeToStatusMap.put(ThingsboardErrorCode.SUBSCRIPTION_VIOLATION, HttpStatus.FORBIDDEN);
errorCodeToStatusMap.put(ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED, HttpStatus.FORBIDDEN);
errorCodeToStatusMap.put(ThingsboardErrorCode.VERSION_CONFLICT, HttpStatus.CONFLICT);
}
@ -143,6 +146,12 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
handleSubscriptionException(thingsboardException, response);
} else if (thingsboardException.getErrorCode() == ThingsboardErrorCode.DATABASE) {
handleDatabaseException(thingsboardException.getCause(), response);
} else if (thingsboardException.getErrorCode() == ThingsboardErrorCode.ENTITIES_LIMIT_EXCEEDED) {
if (thingsboardException.getCause() instanceof EntitiesLimitExceededException entitiesLimitExceededException) {
handleEntitiesLimitExceededException(entitiesLimitExceededException, response);
} else {
handleEntitiesLimitExceededException(thingsboardException, response);
}
} else {
handleThingsboardException(thingsboardException, response);
}
@ -218,6 +227,20 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand
writeResponse(errorResponse, response);
}
private void handleEntitiesLimitExceededException(ThingsboardException entitiesLimitExceededException, HttpServletResponse response) throws IOException {
response.setStatus(HttpStatus.FORBIDDEN.value());
JacksonUtil.writeValue(response.getWriter(),
JacksonUtil.fromBytes(((HttpClientErrorException) entitiesLimitExceededException.getCause()).getResponseBodyAsByteArray(), Object.class));
}
private void handleEntitiesLimitExceededException(EntitiesLimitExceededException entitiesLimitExceededException, HttpServletResponse response) throws IOException {
EntityType entityType = entitiesLimitExceededException.getEntityType();
Long limit = entitiesLimitExceededException.getLimit();
response.setStatus(HttpStatus.FORBIDDEN.value());
JacksonUtil.writeValue(response.getWriter(),
ThingsboardEntitiesLimitExceededResponse.of(entitiesLimitExceededException.getMessage(), entityType, limit));
}
private void handleAccessDeniedException(HttpServletResponse response) throws IOException {
response.setStatus(HttpStatus.FORBIDDEN.value());
JacksonUtil.writeValue(response.getWriter(),

1
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -116,6 +116,7 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createDatabaseIndexes();
// TODO: cleanup update code after each release
systemDataLoaderService.updateDefaultNotificationConfigs(false);
// Runs upgrade scripts that are not possible in plain SQL.
dataUpdateService.updateData();

4
application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java

@ -76,7 +76,7 @@ import org.thingsboard.server.dao.device.provision.ProvisionFailedException;
import org.thingsboard.server.dao.device.provision.ProvisionRequest;
import org.thingsboard.server.dao.device.provision.ProvisionResponse;
import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus;
import org.thingsboard.server.dao.exception.EntitiesLimitException;
import org.thingsboard.server.dao.exception.EntitiesLimitExceededException;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
@ -398,7 +398,7 @@ public class DefaultTransportApiService implements TransportApiService {
} catch (JsonProcessingException e) {
log.warn("[{}] Failed to lookup device by gateway id and name: [{}]", gatewayId, requestMsg.getDeviceName(), e);
throw new RuntimeException(e);
} catch (EntitiesLimitException e) {
} catch (EntitiesLimitExceededException e) {
log.warn("[{}][{}] API limit exception: [{}]", e.getTenantId(), gatewayId, e.getMessage());
return TransportApiResponseMsg.newBuilder()
.setGetOrCreateDeviceResponseMsg(

1
common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java

@ -31,6 +31,7 @@ public enum ThingsboardErrorCode {
TOO_MANY_UPDATES(34),
VERSION_CONFLICT(35),
SUBSCRIPTION_VIOLATION(40),
ENTITIES_LIMIT_EXCEEDED(41),
PASSWORD_VIOLATION(45),
DATABASE(46);

1
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java

@ -32,6 +32,7 @@ public enum NotificationType {
ALARM_ASSIGNMENT,
NEW_PLATFORM_VERSION,
ENTITIES_LIMIT,
ENTITIES_LIMIT_INCREASE_REQUEST,
API_USAGE_LIMIT,
RULE_NODE,
RATE_LIMITS,

50
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntitiesLimitIncreaseRequestNotificationInfo.java

@ -0,0 +1,50 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.notification.info;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.EntityType;
import java.util.Map;
import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EntitiesLimitIncreaseRequestNotificationInfo implements NotificationInfo {
private EntityType entityType;
private String userEmail;
private String increaseLimitActionLabel;
private String increaseLimitLink;
private String baseUrl;
@Override
public Map<String, String> getTemplateData() {
return mapOf(
"entityType", entityType.getNormalName(),
"userEmail", userEmail,
"increaseLimitActionLabel", increaseLimitActionLabel,
"increaseLimitLink", increaseLimitLink,
"baseUrl", baseUrl
);
}
}

8
dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java → dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitExceededException.java

@ -19,7 +19,7 @@ import lombok.Getter;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.TenantId;
public class EntitiesLimitException extends DataValidationException {
public class EntitiesLimitExceededException extends DataValidationException {
private static final long serialVersionUID = -9211462514373279196L;
@Getter
@ -27,9 +27,13 @@ public class EntitiesLimitException extends DataValidationException {
@Getter
private final EntityType entityType;
public EntitiesLimitException(TenantId tenantId, EntityType entityType) {
@Getter
private final long limit;
public EntitiesLimitExceededException(TenantId tenantId, EntityType entityType, long limit) {
super(entityType.getNormalName() + "s limit reached");
this.tenantId = tenantId;
this.entityType = entityType;
this.limit = limit;
}
}

4
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java

@ -177,6 +177,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
defaultNotifications.create(tenantId, DefaultNotifications.entitiesLimitForSysadmin, sysAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.entitiesLimitForTenant, affectedTenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.entitiesLimitIncreaseRequest, sysAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureWarningForSysadmin, sysAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureWarningForTenant, affectedTenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureDisabledForSysadmin, sysAdmins.getId());
@ -225,6 +226,9 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
if (!isNotificationConfigured(tenantId, NotificationType.RESOURCES_SHORTAGE)) {
defaultNotifications.create(tenantId, DefaultNotifications.resourcesShortage, sysAdmins.getId());
}
if (!isNotificationConfigured(tenantId, NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST)) {
defaultNotifications.create(tenantId, DefaultNotifications.entitiesLimitIncreaseRequest, sysAdmins.getId());
}
} else {
var requiredNotificationTypes = List.of(NotificationType.EDGE_CONNECTION, NotificationType.EDGE_COMMUNICATION_FAILURE);
var existingNotificationTypes = notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes(

61
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java

@ -52,12 +52,15 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.RateL
import org.thingsboard.server.common.data.notification.rule.trigger.config.ResourcesShortageNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.TaskProcessingFailureNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.EmailDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig;
import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -102,7 +105,40 @@ public class DefaultNotifications {
.description("Send notification to tenant admins when count of entities of some type reached 80% threshold of the limit")
.build())
.build();
public static final DefaultNotification entitiesLimitIncreaseRequest = DefaultNotification.builder()
.name("Entities limit increase request")
.type(NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST)
.subject("${entityType} limit increase request")
.text("${userEmail} has reached the maximum number of ${entityType:lowerCase}s allowed and is requesting an increase to the ${entityType:lowerCase} limit.")
.button("${increaseLimitActionLabel}").link("${increaseLimitLink}")
.emailTemplate(DefaultEmailTemplate.builder()
.subject("${entityType} limit increase request")
.body("""
<table style="box-sizing: border-box; border-radius: 3px; width: 100%; background-color: #f6f6f6; margin: 0px auto;" cellspacing="0" cellpadding="0" bgcolor="#f6f6f6">
<tbody>
<tr style="box-sizing: border-box; margin: 0px;">
<td style="box-sizing: border-box; vertical-align: middle; margin: 0px; padding: 40px;" align="center" valign="middle">
<table style="box-sizing: border-box; border: 1px solid #E0E0E0; border-radius: 3px; margin: 0px; background-color: #ffffff; max-width: 600px !important;" cellspacing="0" cellpadding="0">
<tbody>
<tr style="box-sizing: border-box; margin: 0px;">
<td style="box-sizing: border-box; vertical-align: middle; border-bottom: 1px solid #E0E0E0; margin: 0px; padding: 20px; color: #212121; font-family: Arial; font-size: 20px; line-height: 20px; font-style: normal; font-weight: bold;" valign="middle">${entityType} limit increase request</td>
</tr>
<tr style="box-sizing: border-box; margin: 0px;">
<td style="box-sizing: border-box; vertical-align: top; margin: 0px; padding: 16px 24px; color: #212121; font-family: Arial; font-size: 16px; line-height: 24px; font-weight: 400;" valign="top">${userEmail} has reached the maximum number of ${entityType:lowerCase}s allowed and is requesting an increase to the ${entityType:lowerCase} limit.</td>
</tr>
<tr style="box-sizing: border-box; margin: 0px;">
<td style="box-sizing: border-box; vertical-align: top; margin: 0px; padding: 0 24px 16px 24px; color: #212121; font-family: Arial; font-size: 16px; line-height: 24px; font-weight: 400;" valign="top">
<a style="display: inline-block; padding: 10px 16px; border-radius: 4px; background: #106CC8; color: #fff; font-family: Arial; font-size: 14px; line-height: 20px; font-weight: bold; text-decoration: none;" href="${baseUrl}${increaseLimitLink}">${increaseLimitActionLabel}</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>""")
.build())
.build();
public static final DefaultNotification apiFeatureWarningForSysadmin = DefaultNotification.builder()
.name("API feature warning notification for sysadmin")
.type(NotificationType.API_USAGE_LIMIT)
@ -415,6 +451,8 @@ public class DefaultNotifications {
private final String button;
private final String link;
private final DefaultEmailTemplate emailTemplate;
private final DefaultRule rule;
public NotificationTemplate toTemplate() {
@ -448,9 +486,17 @@ public class DefaultNotifications {
}
webTemplate.setAdditionalConfig(additionalConfig);
webTemplate.setEnabled(true);
templateConfig.setDeliveryMethodsTemplates(Map.of(
NotificationDeliveryMethod.WEB, webTemplate
));
Map<NotificationDeliveryMethod, DeliveryMethodNotificationTemplate> deliveryMethodsTemplates = new HashMap<>();
deliveryMethodsTemplates.put(NotificationDeliveryMethod.WEB, webTemplate);
if (this.emailTemplate != null) {
EmailDeliveryMethodNotificationTemplate emailTemplate = new EmailDeliveryMethodNotificationTemplate();
emailTemplate.setSubject(this.emailTemplate.getSubject());
emailTemplate.setBody(this.emailTemplate.getBody());
emailTemplate.setEnabled(true);
deliveryMethodsTemplates.put(NotificationDeliveryMethod.EMAIL, emailTemplate);
}
templateConfig.setDeliveryMethodsTemplates(deliveryMethodsTemplates);
template.setConfiguration(templateConfig);
return template;
}
@ -482,6 +528,13 @@ public class DefaultNotifications {
}
@Data
@Builder(toBuilder = true)
public static class DefaultEmailTemplate {
private final String subject;
private final String body;
}
@Data
@Builder(toBuilder = true)
public static class DefaultRule {

5
dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java

@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.TenantEntityWithDataDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.EntitiesLimitException;
import org.thingsboard.server.dao.exception.EntitiesLimitExceededException;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import java.util.HashSet;
@ -123,7 +123,8 @@ public abstract class DataValidator<D extends BaseData<?>> {
protected void validateNumberOfEntitiesPerTenant(TenantId tenantId,
EntityType entityType) {
if (!apiLimitService.checkEntitiesLimit(tenantId, entityType)) {
throw new EntitiesLimitException(tenantId, entityType);
long limit = apiLimitService.getLimit(tenantId, profileConfiguration -> profileConfiguration.getEntitiesLimit(entityType));
throw new EntitiesLimitExceededException(tenantId, entityType, limit);
}
}

5
ui-ngx/src/app/core/http/notification.service.ts

@ -37,6 +37,7 @@ import {
} from '@shared/models/notification.models';
import { User } from '@shared/models/user.model';
import { isNotEmptyStr } from '@core/utils';
import { EntityType } from '@shared/models/entity-type.models';
@Injectable({
providedIn: 'root'
@ -69,6 +70,10 @@ export class NotificationService {
return this.http.post<NotificationRequest>('/api/notification/request', notification, defaultHttpOptionsFromConfig(config));
}
public sendEntitiesLimitIncreaseRequest(entityType: EntityType, config?: RequestConfig): Observable<void> {
return this.http.post<void>(`/api/notification/entitiesLimitIncreaseRequest/${entityType}`, defaultHttpOptionsFromConfig(config));
}
public getNotificationRequestById(id: string, config?: RequestConfig): Observable<NotificationRequest> {
return this.http.get<NotificationRequest>(`/api/notification/request/${id}`, defaultHttpOptionsFromConfig(config));
}

4
ui-ngx/src/app/core/interceptors/global-http-interceptor.ts

@ -109,6 +109,10 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
} else if (errorCode !== Constants.serverErrorCode.credentialsExpired) {
unhandled = true;
}
} else if (errorCode && errorCode === Constants.serverErrorCode.entitiesLimitExceeded) {
if (!ignoreErrors) {
this.dialogService.entitiesLimitExceeded(errorResponse.error);
}
} else if (errorResponse.status === 429) {
if (resendRequest) {
return this.retryRequest(req, next);

15
ui-ngx/src/app/core/services/dialog.service.ts

@ -36,6 +36,11 @@ import {
ErrorAlertDialogData
} from '@shared/components/dialog/error-alert-dialog.component';
import { TodoDialogComponent } from '@shared/components/dialog/todo-dialog.component';
import { EntityType } from '@shared/models/entity-type.models';
import {
EntityLimitExceededDialogComponent,
EntityLimitExceededDialogData
} from '@shared/components/dialog/entity-limit-exceeded-dialog.component';
@Injectable({
providedIn: 'root'
@ -125,6 +130,16 @@ export class DialogService {
}).afterClosed();
}
entitiesLimitExceeded(entityLimitData: {entityType: EntityType, limit: number}): Observable<any> {
return this.dialog.open<EntityLimitExceededDialogComponent, EntityLimitExceededDialogData>(EntityLimitExceededDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: entityLimitData,
autoFocus: false
}).afterClosed();
}
private permissionDenied() {
this.alert(
this.translate.instant('access.permission-denied'),

1
ui-ngx/src/app/modules/home/pages/notification/template/template-notification-dialog.component.ts

@ -181,6 +181,7 @@ export class TemplateNotificationDialogComponent
private allowNotificationType(): NotificationType[] {
const sysAdminAllowNotificationTypes = new Set([
NotificationType.ENTITIES_LIMIT,
NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST,
NotificationType.API_USAGE_LIMIT,
NotificationType.NEW_PLATFORM_VERSION,
NotificationType.RATE_LIMITS,

40
ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.html

@ -0,0 +1,40 @@
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="entity-limit-exceeded-dialog">
<button mat-icon-button class="close-btn" (click)="cancel()">
<tb-icon>close</tb-icon>
</button>
<div class="entity-limit-exceeded-bg"></div>
<div class="entity-limit-exceeded-title">{{ 'entity.limit-reached' | translate }}</div>
<div class="entity-limit-exceeded-container">
<div class="entity-limit-exceeded-text" [innerHTML]="limitReachedText | safe: 'html'"></div>
</div>
<div class="flex flex-row justify-center gap-2">
<button mat-flat-button color="primary" (click)="requestLimitIncrease($event)">
{{ 'entity.request-limit-increase' | translate }}
</button>
<button mat-button (click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
</div>
<div class="request-entity-limit-increase-sysadmin-prompt">
{{ 'entity.request-sysadmin-text' | translate }}
<a href="" (click)="loginAsSysAdmin($event)"> {{ 'entity.login-here' | translate }} </a>
{{ 'entity.to-increase-limit' | translate }}
</div>
</div>

97
ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.scss

@ -0,0 +1,97 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../scss/constants';
.entity-limit-exceeded-dialog {
max-width: 100%;
height: 100%;
@media #{$mat-gt-xs} {
max-width: 500px;
max-height: 80vh;
}
position: relative;
padding: 40px 40px 24px 40px;
display: flex;
flex-direction: column;
gap: 20px;
.close-btn {
position: absolute;
top: 8px;
right: 8px;
color: rgba(0, 0, 0, 0.54);
}
.entity-limit-exceeded-bg {
position: relative;
width: 100%;
height: 140px;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: $tb-primary-color;
mask-image: url(/assets/entity-limit-reached.svg);
-webkit-mask-image: url(/assets/entity-limit-reached.svg);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: contain;
-webkit-mask-size: contain;
mask-position: center;
-webkit-mask-position: center;
}
}
.entity-limit-exceeded-title {
font-size: 24px;
font-style: normal;
font-weight: 500;
line-height: 32px;
color: rgba(0, 0, 0, 0.87);
text-align: center;
}
.entity-limit-exceeded-container {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
justify-content: center;
.entity-limit-exceeded-text {
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
color: rgba(0, 0, 0, 0.76);
text-align: center;
}
}
.request-entity-limit-increase-sysadmin-prompt {
text-align: center;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
color: rgba(0, 0, 0, 0.54);
}
}

100
ui-ngx/src/app/shared/components/dialog/entity-limit-exceeded-dialog.component.ts

@ -0,0 +1,100 @@
///
/// Copyright © 2016-2025 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { Component, Inject, ViewEncapsulation } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { DialogService } from '@core/services/dialog.service';
import { AuthService } from '@core/auth/auth.service';
import { TenantService } from '@core/http/tenant.service';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { NotificationService } from '@core/http/notification.service';
export interface EntityLimitExceededDialogData {
entityType: EntityType;
limit: number;
}
// @dynamic
@Component({
selector: 'tb-entity-limit-exceeded-dialog',
templateUrl: './entity-limit-exceeded-dialog.component.html',
styleUrls: ['./entity-limit-exceeded-dialog.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class EntityLimitExceededDialogComponent extends DialogComponent<EntityLimitExceededDialogComponent> {
limitReachedText: string;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: EntityLimitExceededDialogData,
public dialogRef: MatDialogRef<EntityLimitExceededDialogComponent>,
private authService: AuthService,
private dialogs: DialogService,
private translate: TranslateService,
private tenantService: TenantService,
private notificationService: NotificationService) {
super(store, router, dialogRef);
let entitiesText: string;
if (data.limit > 1) {
entitiesText = data.limit + ' ' + (this.translate.instant(entityTypeTranslations.get(data.entityType).typePlural) as string).toLowerCase();
} else {
entitiesText = '1 ' + (this.translate.instant(entityTypeTranslations.get(data.entityType).type) as string).toLowerCase();
}
this.limitReachedText = this.translate.instant('entity.limit-reached-text', { entities: entitiesText, entity: (this.translate.instant(entityTypeTranslations.get(data.entityType).type) as string).toLowerCase() });
}
cancel(): void {
this.dialogRef.close();
}
requestLimitIncrease($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.notificationService.sendEntitiesLimitIncreaseRequest(this.data.entityType).subscribe(
() => {
this.dialogRef.close();
this.dialogs.alert(
this.translate.instant('entity.increase-limit-request-sent-title'),
this.translate.instant('entity.increase-limit-request-sent-text'),
this.translate.instant('action.close')
);
}
);
}
loginAsSysAdmin($event: Event) {
if ($event) {
$event.preventDefault();
$event.stopPropagation();
}
this.tenantService.getTenant(getCurrentAuthUser(this.store).tenantId).subscribe(
(tenant) => {
this.authService.redirectUrl = `/tenantProfiles/${tenant.tenantProfileId.id}`;
this.authService.logout();
}
);
}
}

2
ui-ngx/src/app/shared/components/notification/notification.component.ts

@ -157,6 +157,8 @@ export class NotificationComponent implements OnInit {
return {color: AlarmSeverityNotificationColors.get(this.notification.info.alarmSeverity)};
} else if (this.notification.type === NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT) {
return {color: '#D12730'};
} else if (this.notification.type === NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST) {
return {color: '#305680'};
}
return null;
}

2
ui-ngx/src/app/shared/models/constants.ts

@ -31,6 +31,7 @@ export const Constants = {
itemNotFound: 32,
tooManyRequests: 33,
tooManyUpdates: 34,
entitiesLimitExceeded: 41,
passwordViolation: 45
},
entryPoints: {
@ -52,6 +53,7 @@ export const serverErrorCodesTranslations = new Map<number, string>([
[Constants.serverErrorCode.itemNotFound, 'server-error.item-not-found'],
[Constants.serverErrorCode.tooManyRequests, 'server-error.too-many-requests'],
[Constants.serverErrorCode.tooManyUpdates, 'server-error.too-many-updates'],
[Constants.serverErrorCode.entitiesLimitExceeded, 'server-error.entities-limit-exceeded'],
]);
export const MediaBreakpoints = {

8
ui-ngx/src/app/shared/models/notification.models.ts

@ -526,6 +526,7 @@ export enum NotificationType {
ALARM_ASSIGNMENT = 'ALARM_ASSIGNMENT',
RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT = 'RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT',
ENTITIES_LIMIT = 'ENTITIES_LIMIT',
ENTITIES_LIMIT_INCREASE_REQUEST = 'ENTITIES_LIMIT_INCREASE_REQUEST',
API_USAGE_LIMIT = 'API_USAGE_LIMIT',
NEW_PLATFORM_VERSION = 'NEW_PLATFORM_VERSION',
RULE_NODE = 'RULE_NODE',
@ -544,6 +545,7 @@ export const NotificationTypeIcons = new Map<NotificationType, string | null>([
[NotificationType.ALARM_ASSIGNMENT, 'assignment_turned_in'],
[NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, 'settings_ethernet'],
[NotificationType.ENTITIES_LIMIT, 'data_thresholding'],
[NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST, 'mdi:file-cog'],
[NotificationType.API_USAGE_LIMIT, 'insert_chart'],
[NotificationType.TASK_PROCESSING_FAILURE, 'warning'],
[NotificationType.RESOURCES_SHORTAGE, 'warning']
@ -623,6 +625,12 @@ export const NotificationTemplateTypeTranslateMap = new Map<NotificationType, No
helpId: 'notification/entities_limit'
}
],
[NotificationType.ENTITIES_LIMIT_INCREASE_REQUEST,
{
name: 'notification.template-type.entities-limit-increase-request',
helpId: 'notification/entities_limit_increase_request'
}
],
[NotificationType.API_USAGE_LIMIT,
{
name: 'notification.template-type.api-usage-limit',

3
ui-ngx/src/app/shared/shared.module.ts

@ -236,6 +236,7 @@ import { PasswordRequirementsTooltipComponent } from '@shared/components/passwor
import { StringPatternAutocompleteComponent } from '@shared/components/string-pattern-autocomplete.component';
import { TimeUnitInputComponent } from '@shared/components/time-unit-input.component';
import { DateExpirationPipe } from '@shared/pipe/date-expiration.pipe';
import { EntityLimitExceededDialogComponent } from '@shared/components/dialog/entity-limit-exceeded-dialog.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -374,6 +375,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
TodoDialogComponent,
ColorPickerDialogComponent,
MaterialIconsDialogComponent,
EntityLimitExceededDialogComponent,
ColorInputComponent,
MaterialIconSelectComponent,
NodeScriptTestDialogComponent,
@ -643,6 +645,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
TodoDialogComponent,
ColorPickerDialogComponent,
MaterialIconsDialogComponent,
EntityLimitExceededDialogComponent,
ColorInputComponent,
MaterialIconSelectComponent,
NodeScriptTestDialogComponent,

20
ui-ngx/src/assets/entity-limit-reached.svg

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" width="140" height="140" viewBox="0 0 140 140" fill="none">
<g clip-path="url(#clip0_13185_50044)">
<path opacity="0.04" fill-rule="evenodd" clip-rule="evenodd" d="M102.271 129.383C86.4025 136.01 69.2411 130.203 52.8508 125.028C36.2959 119.802 17.7835 115.275 9.42417 100.07C0.9147 84.5925 1.6114 64.0191 11.1144 49.1143C19.4626 36.0208 38.857 39.4065 52.6528 32.2694C66.601 25.0534 75.3719 5.2002 90.8419 7.86788C106.558 10.578 111.71 29.9276 119.548 43.8049C127.943 58.6713 140.512 73.089 137.132 89.8316C133.518 107.731 119.131 122.342 102.271 129.383Z" fill="#305680"/>
<path d="M125.137 21.6824L122.365 25.7374C122.001 26.2698 121.172 26.0687 121.093 25.4288L120.236 18.5327C109.745 31.9955 93.1047 49.4609 73.2087 61.1045C39.4675 80.8506 24.7349 82.2084 17.5 83.3C26.5436 81.9355 55.1215 71.2926 76.8262 55.6465C91.6577 44.9551 108.298 25.8099 115.895 16.3495L108.955 17.5129C108.329 17.6178 107.898 16.9037 108.282 16.3989L111.746 11.8419C111.855 11.699 112.014 11.6033 112.191 11.5745L122.455 9.9095C122.83 9.84861 123.185 10.0984 123.255 10.4722L125.247 21.1591C125.281 21.3413 125.241 21.5295 125.137 21.6824Z" fill="#305680"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.7541 109.935C17.5926 109.803 17.5 109.605 17.5 109.397V95.0984C17.5 94.7523 17.7529 94.4584 18.0949 94.4052C22.6363 93.6985 27.0488 92.8106 31.3297 91.7665C31.7721 91.6586 32.2 91.9927 32.2 92.4481V117.07C32.2 117.568 31.6956 117.906 31.2373 117.713C26.3129 115.632 21.7004 113.15 17.7541 109.935ZM39.675 120.867C39.3909 120.771 39.2 120.505 39.2 120.205V90.1542C39.2 89.8448 39.4032 89.5723 39.6994 89.4827C44.279 88.098 48.6919 86.5389 52.9345 84.8406C53.3961 84.6558 53.9 84.9947 53.9 85.4919V124.404C53.9 124.877 53.4401 125.214 52.9888 125.072C52.9428 125.057 52.8967 125.043 52.8507 125.028C51.4223 124.577 49.9792 124.131 48.5294 123.683C45.5842 122.774 42.611 121.855 39.675 120.867ZM75.2387 73.8871C75.7073 73.6117 76.3 73.9484 76.3 74.492V130.563C76.3 130.997 75.9089 131.327 75.4818 131.249C71.0195 130.432 66.5452 129.234 62.0986 127.908C61.8025 127.819 61.6 127.547 61.6 127.238V81.5218C61.6 81.2503 61.7571 81.0034 62.0025 80.8875C66.6525 78.691 71.0666 76.3395 75.2387 73.8871ZM84.6641 132.313C84.2918 132.298 84 131.99 84 131.618V68.7407C84 68.5078 84.116 68.2902 84.3088 68.1597C89.121 64.9013 93.5358 61.5462 97.5426 58.2079C98.0009 57.8261 98.7 58.1505 98.7 58.747V130.143C98.7 130.452 98.4979 130.725 98.2012 130.81C93.7597 132.084 89.2328 132.493 84.6641 132.313ZM119.793 35.0901C120.199 34.5339 121.1 34.8172 121.1 35.5056V116.978C121.1 117.173 121.019 117.359 120.876 117.491C116.833 121.208 112.27 124.403 107.411 126.962C106.949 127.204 106.4 126.867 106.4 126.345V50.5478C106.4 50.3585 106.477 50.1767 106.613 50.0446C112.225 44.5776 116.64 39.4155 119.793 35.0901Z" fill="#305680"/>
<path opacity="0.16" d="M88.2 23.8C88.2 24.9598 87.2598 25.9 86.1 25.9C84.9402 25.9 84 24.9598 84 23.8C84 22.6402 84.9402 21.7 86.1 21.7C87.2598 21.7 88.2 22.6402 88.2 23.8Z" fill="#305680"/>
<path opacity="0.16" d="M28.7 59.85C28.7 60.4299 28.2299 60.9 27.65 60.9C27.0701 60.9 26.6 60.4299 26.6 59.85C26.6 59.2701 27.0701 58.8 27.65 58.8C28.2299 58.8 28.7 59.2701 28.7 59.85Z" fill="#305680"/>
<path opacity="0.16" d="M82.6 89.25C82.6 89.8299 82.1299 90.3 81.55 90.3C80.9701 90.3 80.5 89.8299 80.5 89.25C80.5 88.6701 80.9701 88.2 81.55 88.2C82.1299 88.2 82.6 88.6701 82.6 89.25Z" fill="#305680"/>
<path opacity="0.16" d="M38.5 85.4C38.5 85.7866 38.1866 86.1 37.8 86.1C37.4134 86.1 37.1 85.7866 37.1 85.4C37.1 85.0134 37.4134 84.7 37.8 84.7C38.1866 84.7 38.5 85.0134 38.5 85.4Z" fill="#305680"/>
<path opacity="0.16" d="M129.5 82.6C129.5 83.3732 128.873 84 128.1 84C127.327 84 126.7 83.3732 126.7 82.6C126.7 81.8268 127.327 81.2 128.1 81.2C128.873 81.2 129.5 81.8268 129.5 82.6Z" fill="#305680"/>
<path opacity="0.16" d="M53.2 48.3C53.2 49.0732 52.5732 49.7 51.8 49.7C51.0268 49.7 50.4 49.0732 50.4 48.3C50.4 47.5268 51.0268 46.9 51.8 46.9C52.5732 46.9 53.2 47.5268 53.2 48.3Z" fill="#305680"/>
<path opacity="0.16" d="M16.1 77C16.1 77.3866 15.7866 77.7 15.4 77.7C15.0134 77.7 14.7 77.3866 14.7 77C14.7 76.6134 15.0134 76.3 15.4 76.3C15.7866 76.3 16.1 76.6134 16.1 77Z" fill="#305680"/>
<path opacity="0.16" d="M79.1 43.4C79.1 44.1732 78.4732 44.8 77.7 44.8C76.9269 44.8 76.3 44.1732 76.3 43.4C76.3 42.6268 76.9269 42 77.7 42C78.4732 42 79.1 42.6268 79.1 43.4Z" fill="#305680"/>
</g>
<defs>
<clipPath id="clip0_13185_50044">
<rect width="140" height="140" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

46
ui-ngx/src/assets/help/en_US/notification/entities_limit_increase_request.md

@ -0,0 +1,46 @@
#### Entity count limit increase request notification templatization
<div class="divider"></div>
<br/>
Notification subject and message fields support templatization.
The list of available templatization parameters depends on the template type.
See the available types and parameters below:
Available template parameters:
* `entityType` - one of: 'Device', 'Asset', 'Customer', 'User', 'Dashboard', 'Rule chain', 'Edge';
* `userEmail` - email of the user who sends the request;
* `increaseLimitActionLabel` - label of the button used to open Limits Management page, for ex: 'Set new limit';
* `increaseLimitLink` - link to the Limits Management page;
* `baseUrl` - used to construct the full URL for the Limits Management page in email notifications;
Parameter names must be wrapped using `${...}`. For example: `${userEmail}`.
You may also modify the value of the parameter with one of the suffixes:
* `upperCase`, for example - `${userEmail:upperCase}`
* `lowerCase`, for example - `${userEmail:lowerCase}`
* `capitalize`, for example - `${userEmail:capitalize}`
<div class="divider"></div>
##### Examples
Let's assume the notification about the increasing limit of the maximum allowed devices for the tenant.
The following template:
```text
${userEmail} has reached the maximum number of ${entityType:lowerCase}s allowed and is requesting an increase to the ${entityType:lowerCase} limit.
{:copy-code}
```
will be transformed to:
```text
johndoe@company.com has reached the maximum number of devices allowed and is requesting an increase to the device limit.
```
<br/>
<br>
<br>

14
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3179,7 +3179,15 @@
"list-of-mobile-apps": "{ count, plural, =1 {One Mobile application} other {List of # Mobile applications} }",
"type-mobile-app-bundle": "Mobile bundle",
"type-mobile-app-bundles": "Mobile bundles",
"list-of-mobile-app-bundles": "{ count, plural, =1 {One mobile bundle} other {List of # mobile bundles} }"
"list-of-mobile-app-bundles": "{ count, plural, =1 {One mobile bundle} other {List of # mobile bundles} }",
"limit-reached": "Limit reached",
"limit-reached-text": "You have reached the limit of <b>{{ entities }}</b>. To add more, please ask your System Administrator to increase your {{ entity }} limit.",
"request-limit-increase": "Request limit increase",
"request-sysadmin-text": "Are you the System Administrator?",
"login-here": "Login here",
"to-increase-limit": "to increase limit.",
"increase-limit-request-sent-title": "We have sent an automated request to your System Administrator to increase the limit",
"increase-limit-request-sent-text": "Please allow some time for them to review the request and update the settings. You may need to refresh this page to see the changes."
},
"entity-field": {
"created-time": "Created time",
@ -4705,6 +4713,7 @@
"api-usage-limit": "API usage limit",
"device-activity": "Device activity",
"entities-limit": "Entities limit",
"entities-limit-increase-request": "Entities limit increase request",
"entity-action": "Entity action",
"general": "General",
"rule-engine-lifecycle-event": "Rule engine lifecycle event",
@ -6133,7 +6142,8 @@
"bad-request-params": "Bad request params",
"item-not-found": "Item not found",
"too-many-requests": "Too many requests",
"too-many-updates": "Too many updates"
"too-many-updates": "Too many updates",
"entities-limit-exceeded": "Entities limit exceeded"
},
"tenant": {
"tenant": "Tenant",

Loading…
Cancel
Save