Browse Source

Merge branch 'rc' into features/add_tooltip_option_to_show_stack_mode_total_value_on_timeseries_chart_widgets

pull/13373/head
Paolo Cristiani 1 year ago
committed by GitHub
parent
commit
da8bca8d4b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      application/src/main/data/json/demo/dashboards/rule_engine_statistics.json
  2. 33
      application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java
  3. 7
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java
  4. 25
      application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java
  5. 40
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  6. 1
      common/data/src/main/java/org/thingsboard/server/common/data/SystemInfoData.java
  7. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/ResourcesShortageNotificationInfo.java
  8. 9
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java
  9. 9
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java
  10. 9
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java
  11. 11
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTrigger.java
  12. 8
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java
  13. 8
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java
  14. 3
      common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java
  15. 2
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
  16. 11
      ui-ngx/src/app/core/http/ota-package.service.ts
  17. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss
  18. 18
      ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts
  19. 2
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts
  20. 6
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.scss
  21. 2
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html
  22. 1
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
  23. 6
      ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts
  24. 11
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts
  25. 7
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts
  26. 9
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts
  27. 1
      ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts
  28. 9
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts
  29. 6
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html
  30. 2
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.ts
  31. 15
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.html
  32. 6
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts
  33. 2
      ui-ngx/src/assets/dashboard/api_usage.json
  34. 6
      ui-ngx/src/assets/help/en_US/notification/resources_shortage.md
  35. 2
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  36. 24
      ui-ngx/src/scss/animations.scss

2
application/src/main/data/json/demo/dashboards/rule_engine_statistics.json

@ -564,6 +564,7 @@
}, },
"tooltipDateColor": "rgba(0, 0, 0, 0.76)", "tooltipDateColor": "rgba(0, 0, 0, 0.76)",
"tooltipDateInterval": true, "tooltipDateInterval": true,
"tooltipHideZeroFalse": true,
"tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)",
"tooltipBackgroundBlur": 4, "tooltipBackgroundBlur": 4,
"animation": { "animation": {
@ -977,6 +978,7 @@
}, },
"tooltipDateColor": "rgba(0, 0, 0, 0.76)", "tooltipDateColor": "rgba(0, 0, 0, 0.76)",
"tooltipDateInterval": true, "tooltipDateInterval": true,
"tooltipHideZeroFalse": true,
"tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)",
"tooltipBackgroundBlur": 4, "tooltipBackgroundBlur": 4,
"animation": { "animation": {

33
application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequestStatus
import org.thingsboard.server.common.data.notification.info.NotificationInfo; import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger.DeduplicationStrategy;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@ -66,8 +67,8 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
private final NotificationDeduplicationService deduplicationService; private final NotificationDeduplicationService deduplicationService;
private final PartitionService partitionService; private final PartitionService partitionService;
private final RateLimitService rateLimitService; private final RateLimitService rateLimitService;
@Autowired @Lazy @Lazy
private NotificationCenter notificationCenter; private final NotificationCenter notificationCenter;
private final NotificationExecutorService notificationExecutor; private final NotificationExecutorService notificationExecutor;
private final Map<NotificationRuleTriggerType, NotificationRuleTriggerProcessor> triggerProcessors = new EnumMap<>(NotificationRuleTriggerType.class); private final Map<NotificationRuleTriggerType, NotificationRuleTriggerProcessor> triggerProcessors = new EnumMap<>(NotificationRuleTriggerType.class);
@ -82,14 +83,11 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
if (enabledRules.isEmpty()) { if (enabledRules.isEmpty()) {
return; return;
} }
if (trigger.deduplicate()) {
enabledRules = new ArrayList<>(enabledRules); List<NotificationRule> rulesToProcess = filterNotificationRules(trigger, enabledRules);
enabledRules.removeIf(rule -> deduplicationService.alreadyProcessed(trigger, rule)); for (NotificationRule rule : rulesToProcess) {
}
final List<NotificationRule> rules = enabledRules;
for (NotificationRule rule : rules) {
try { try {
processNotificationRule(rule, trigger); processNotificationRule(rule, trigger, DeduplicationStrategy.ONLY_MATCHING.equals(trigger.getDeduplicationStrategy()));
} catch (Throwable e) { } catch (Throwable e) {
log.error("Failed to process notification rule {} for trigger type {} with trigger object {}", rule.getId(), rule.getTriggerType(), trigger, e); log.error("Failed to process notification rule {} for trigger type {} with trigger object {}", rule.getId(), rule.getTriggerType(), trigger, e);
} }
@ -100,7 +98,20 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
}); });
} }
private void processNotificationRule(NotificationRule rule, NotificationRuleTrigger trigger) { private List<NotificationRule> filterNotificationRules(NotificationRuleTrigger trigger, List<NotificationRule> enabledRules) {
List<NotificationRule> rulesToProcess = new ArrayList<>(enabledRules);
rulesToProcess.removeIf(rule -> switch (trigger.getDeduplicationStrategy()) {
case ONLY_MATCHING -> {
boolean matched = matchesFilter(trigger, rule.getTriggerConfig());
yield !matched || deduplicationService.alreadyProcessed(trigger, rule);
}
case ALL -> deduplicationService.alreadyProcessed(trigger, rule);
default -> false;
});
return rulesToProcess;
}
private void processNotificationRule(NotificationRule rule, NotificationRuleTrigger trigger, boolean alreadyMatched) {
NotificationRuleTriggerConfig triggerConfig = rule.getTriggerConfig(); NotificationRuleTriggerConfig triggerConfig = rule.getTriggerConfig();
log.debug("Processing notification rule '{}' for trigger type {}", rule.getName(), rule.getTriggerType()); log.debug("Processing notification rule '{}' for trigger type {}", rule.getName(), rule.getTriggerType());
@ -114,7 +125,7 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
return; return;
} }
if (matchesFilter(trigger, triggerConfig)) { if (alreadyMatched || matchesFilter(trigger, triggerConfig)) {
if (!rateLimitService.checkRateLimit(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, rule.getTenantId(), rule.getId())) { if (!rateLimitService.checkRateLimit(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, rule.getTenantId(), rule.getId())) {
log.debug("[{}] Rate limit for notification requests per rule was exceeded (rule '{}')", rule.getTenantId(), rule.getName()); log.debug("[{}] Rate limit for notification requests per rule was exceeded (rule '{}')", rule.getTenantId(), rule.getName());
return; return;

7
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java

@ -39,7 +39,12 @@ public class ResourcesShortageTriggerProcessor implements NotificationRuleTrigge
@Override @Override
public RuleOriginatedNotificationInfo constructNotificationInfo(ResourcesShortageTrigger trigger) { public RuleOriginatedNotificationInfo constructNotificationInfo(ResourcesShortageTrigger trigger) {
return ResourcesShortageNotificationInfo.builder().resource(trigger.getResource().name()).usage(trigger.getUsage()).build(); return ResourcesShortageNotificationInfo.builder()
.resource(trigger.getResource().name())
.usage(trigger.getUsage())
.serviceId(trigger.getServiceId())
.serviceType(trigger.getServiceType())
.build();
} }
@Override @Override

25
application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java

@ -185,9 +185,14 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
List<SystemInfoData> clusterSystemData = getSystemData(serviceInfoProvider.getServiceInfo()); List<SystemInfoData> clusterSystemData = getSystemData(serviceInfoProvider.getServiceInfo());
clusterSystemData.forEach(data -> { clusterSystemData.forEach(data -> {
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.CPU).usage(data.getCpuUsage()).build()); Arrays.stream(Resource.values()).forEach(resource -> {
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.RAM).usage(data.getMemoryUsage()).build()); notificationRuleProcessor.process(ResourcesShortageTrigger.builder()
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.STORAGE).usage(data.getDiscUsage()).build()); .resource(resource)
.serviceId(data.getServiceId())
.serviceType(data.getServiceType())
.usage(extractResourceUsage(data, resource))
.build());
});
}); });
BasicTsKvEntry clusterDataKv = new BasicTsKvEntry(ts, new JsonDataEntry("clusterSystemData", JacksonUtil.toString(clusterSystemData))); BasicTsKvEntry clusterDataKv = new BasicTsKvEntry(ts, new JsonDataEntry("clusterSystemData", JacksonUtil.toString(clusterSystemData)));
doSave(Arrays.asList(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", true)), clusterDataKv)); doSave(Arrays.asList(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", true)), clusterDataKv));
@ -200,17 +205,17 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
getCpuUsage().ifPresent(v -> { getCpuUsage().ifPresent(v -> {
long value = (long) v; long value = (long) v;
tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuUsage", value))); tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuUsage", value)));
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.CPU).usage(value).build()); notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.CPU).usage(value).serviceId(serviceInfoProvider.getServiceId()).serviceType(serviceInfoProvider.getServiceType()).build());
}); });
getMemoryUsage().ifPresent(v -> { getMemoryUsage().ifPresent(v -> {
long value = (long) v; long value = (long) v;
tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", value))); tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", value)));
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.RAM).usage(value).build()); notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.RAM).usage(value).serviceId(serviceInfoProvider.getServiceId()).serviceType(serviceInfoProvider.getServiceType()).build());
}); });
getDiscSpaceUsage().ifPresent(v -> { getDiscSpaceUsage().ifPresent(v -> {
long value = (long) v; long value = (long) v;
tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("discUsage", value))); tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("discUsage", value)));
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.STORAGE).usage(value).build()); notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.STORAGE).usage(value).serviceId(serviceInfoProvider.getServiceId()).serviceType(serviceInfoProvider.getServiceType()).build());
}); });
getCpuCount().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuCount", (long) v)))); getCpuCount().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuCount", (long) v))));
@ -258,6 +263,14 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
return infoData; return infoData;
} }
private Long extractResourceUsage(SystemInfoData info, Resource resource) {
return switch (resource) {
case CPU -> info.getCpuUsage();
case RAM -> info.getMemoryUsage();
case STORAGE -> info.getDiscUsage();
};
}
@PreDestroy @PreDestroy
private void destroy() { private void destroy() {
if (scheduler != null) { if (scheduler != null) {

40
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -825,6 +825,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
notificationRuleProcessor.process(ResourcesShortageTrigger.builder() notificationRuleProcessor.process(ResourcesShortageTrigger.builder()
.resource(Resource.RAM) .resource(Resource.RAM)
.usage(15L) .usage(15L)
.serviceType("serviceType")
.serviceId("serviceId")
.build()); .build());
TimeUnit.MILLISECONDS.sleep(300); TimeUnit.MILLISECONDS.sleep(300);
} }
@ -837,10 +839,48 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
notificationRuleProcessor.process(ResourcesShortageTrigger.builder() notificationRuleProcessor.process(ResourcesShortageTrigger.builder()
.resource(Resource.RAM) .resource(Resource.RAM)
.usage(5L) .usage(5L)
.serviceType("serviceType")
.serviceId("serviceId")
.build()); .build());
await("").atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertThat(getMyNotifications(false, 100)).size().isOne()); await("").atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertThat(getMyNotifications(false, 100)).size().isOne());
} }
@Test
public void testNotificationsResourcesShortage_whenThresholdChangeToMatchingFilter_thenSendNotification() throws Exception {
loginSysAdmin();
ResourcesShortageNotificationRuleTriggerConfig triggerConfig = ResourcesShortageNotificationRuleTriggerConfig.builder()
.ramThreshold(1f)
.cpuThreshold(1f)
.storageThreshold(1f)
.build();
NotificationRule rule = createNotificationRule(triggerConfig, "Warning: ${resource} shortage", "${resource} shortage", createNotificationTarget(tenantAdminUserId).getId());
loginTenantAdmin();
Method method = DefaultSystemInfoService.class.getDeclaredMethod("saveCurrentMonolithSystemInfo");
method.setAccessible(true);
method.invoke(systemInfoService);
TimeUnit.SECONDS.sleep(5);
assertThat(getMyNotifications(false, 100)).size().isZero();
loginSysAdmin();
triggerConfig = ResourcesShortageNotificationRuleTriggerConfig.builder()
.ramThreshold(0.01f)
.cpuThreshold(1f)
.storageThreshold(1f)
.build();
rule.setTriggerConfig(triggerConfig);
saveNotificationRule(rule);
loginTenantAdmin();
method.invoke(systemInfoService);
await().atMost(10, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1);
Notification notification = getMyNotifications(false, 100).get(0);
assertThat(notification.getSubject()).isEqualTo("Warning: RAM shortage");
assertThat(notification.getText()).isEqualTo("RAM shortage");
}
@Test @Test
public void testNotificationRuleDisabling() throws Exception { public void testNotificationRuleDisabling() throws Exception {
EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig(); EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig();

1
common/data/src/main/java/org/thingsboard/server/common/data/SystemInfoData.java

@ -20,6 +20,7 @@ import lombok.Data;
@Data @Data
public class SystemInfoData { public class SystemInfoData {
@Schema(description = "Service Id.") @Schema(description = "Service Id.")
private String serviceId; private String serviceId;
@Schema(description = "Service type.") @Schema(description = "Service type.")

6
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/ResourcesShortageNotificationInfo.java

@ -30,12 +30,16 @@ public class ResourcesShortageNotificationInfo implements RuleOriginatedNotifica
private String resource; private String resource;
private Long usage; private Long usage;
private String serviceId;
private String serviceType;
@Override @Override
public Map<String, String> getTemplateData() { public Map<String, String> getTemplateData() {
return Map.of( return Map.of(
"resource", resource, "resource", resource,
"usage", String.valueOf(usage) "usage", String.valueOf(usage),
"serviceId", serviceId,
"serviceType", serviceType
); );
} }

9
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java

@ -23,12 +23,16 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.io.Serial;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Data @Data
@Builder @Builder
public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger { public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger {
@Serial
private static final long serialVersionUID = 2918443863787603524L;
private final TenantId tenantId; private final TenantId tenantId;
private final CustomerId customerId; private final CustomerId customerId;
private final EdgeId edgeId; private final EdgeId edgeId;
@ -37,8 +41,8 @@ public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger
private final String error; private final String error;
@Override @Override
public boolean deduplicate() { public DeduplicationStrategy getDeduplicationStrategy() {
return true; return DeduplicationStrategy.ALL;
} }
@Override @Override
@ -60,4 +64,5 @@ public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger
public EntityId getOriginatorEntityId() { public EntityId getOriginatorEntityId() {
return edgeId; return edgeId;
} }
} }

9
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java

@ -23,12 +23,16 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.io.Serial;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Data @Data
@Builder @Builder
public class EdgeConnectionTrigger implements NotificationRuleTrigger { public class EdgeConnectionTrigger implements NotificationRuleTrigger {
@Serial
private static final long serialVersionUID = -261939829962721957L;
private final TenantId tenantId; private final TenantId tenantId;
private final CustomerId customerId; private final CustomerId customerId;
private final EdgeId edgeId; private final EdgeId edgeId;
@ -36,8 +40,8 @@ public class EdgeConnectionTrigger implements NotificationRuleTrigger {
private final String edgeName; private final String edgeName;
@Override @Override
public boolean deduplicate() { public DeduplicationStrategy getDeduplicationStrategy() {
return true; return DeduplicationStrategy.ALL;
} }
@Override @Override
@ -59,4 +63,5 @@ public class EdgeConnectionTrigger implements NotificationRuleTrigger {
public EntityId getOriginatorEntityId() { public EntityId getOriginatorEntityId() {
return edgeId; return edgeId;
} }
} }

9
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java

@ -22,10 +22,15 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.io.Serial;
@Data @Data
@Builder @Builder
public class NewPlatformVersionTrigger implements NotificationRuleTrigger { public class NewPlatformVersionTrigger implements NotificationRuleTrigger {
@Serial
private static final long serialVersionUID = 3298785969736390092L;
private final UpdateMessage updateInfo; private final UpdateMessage updateInfo;
@Override @Override
@ -45,8 +50,8 @@ public class NewPlatformVersionTrigger implements NotificationRuleTrigger {
@Override @Override
public boolean deduplicate() { public DeduplicationStrategy getDeduplicationStrategy() {
return true; return DeduplicationStrategy.ALL;
} }
@Override @Override

11
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTrigger.java

@ -29,9 +29,8 @@ public interface NotificationRuleTrigger extends Serializable {
EntityId getOriginatorEntityId(); EntityId getOriginatorEntityId();
default DeduplicationStrategy getDeduplicationStrategy() {
default boolean deduplicate() { return DeduplicationStrategy.NONE;
return false;
} }
default String getDeduplicationKey() { default String getDeduplicationKey() {
@ -43,4 +42,10 @@ public interface NotificationRuleTrigger extends Serializable {
return 0; return 0;
} }
enum DeduplicationStrategy {
NONE,
ALL,
ONLY_MATCHING
}
} }

8
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java

@ -22,12 +22,16 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.io.Serial;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Data @Data
@Builder @Builder
public class RateLimitsTrigger implements NotificationRuleTrigger { public class RateLimitsTrigger implements NotificationRuleTrigger {
@Serial
private static final long serialVersionUID = -4423112145409424886L;
private final TenantId tenantId; private final TenantId tenantId;
private final LimitedApi api; private final LimitedApi api;
private final EntityId limitLevel; private final EntityId limitLevel;
@ -45,8 +49,8 @@ public class RateLimitsTrigger implements NotificationRuleTrigger {
@Override @Override
public boolean deduplicate() { public DeduplicationStrategy getDeduplicationStrategy() {
return true; return DeduplicationStrategy.ALL;
} }
@Override @Override

8
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java

@ -33,6 +33,8 @@ public class ResourcesShortageTrigger implements NotificationRuleTrigger {
private Resource resource; private Resource resource;
private Long usage; private Long usage;
private String serviceId;
private String serviceType;
@Override @Override
public TenantId getTenantId() { public TenantId getTenantId() {
@ -45,13 +47,13 @@ public class ResourcesShortageTrigger implements NotificationRuleTrigger {
} }
@Override @Override
public boolean deduplicate() { public DeduplicationStrategy getDeduplicationStrategy() {
return true; return DeduplicationStrategy.ONLY_MATCHING;
} }
@Override @Override
public String getDeduplicationKey() { public String getDeduplicationKey() {
return resource.name(); return String.join(":", resource.name(), serviceId, serviceType);
} }
@Override @Override

3
common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java

@ -22,6 +22,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.JavaSerDesUtil; import org.thingsboard.server.common.data.JavaSerDesUtil;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger.DeduplicationStrategy;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
@ -47,7 +48,7 @@ public class RemoteNotificationRuleProcessor implements NotificationRuleProcesso
@Override @Override
public void process(NotificationRuleTrigger trigger) { public void process(NotificationRuleTrigger trigger) {
try { try {
if (trigger.deduplicate() && deduplicationService.alreadyProcessed(trigger)) { if (!DeduplicationStrategy.NONE.equals(trigger.getDeduplicationStrategy()) && deduplicationService.alreadyProcessed(trigger)) {
return; return;
} }

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

@ -376,7 +376,7 @@ public class DefaultNotifications {
public static final DefaultNotification resourcesShortage = DefaultNotification.builder() public static final DefaultNotification resourcesShortage = DefaultNotification.builder()
.name("Resources shortage notification") .name("Resources shortage notification")
.type(NotificationType.RESOURCES_SHORTAGE) .type(NotificationType.RESOURCES_SHORTAGE)
.subject("Warning: ${resource} shortage") .subject("Warning: ${resource} shortage for ${serviceId}")
.text("${resource} usage is at ${usage}%.") .text("${resource} usage is at ${usage}%.")
.icon("warning") .icon("warning")
.rule(DefaultRule.builder() .rule(DefaultRule.builder()

11
ui-ngx/src/app/core/http/ota-package.service.ts

@ -129,15 +129,16 @@ export class OtaPackageService {
} }
return forkJoin(tasks).pipe( return forkJoin(tasks).pipe(
mergeMap(([deviceFirmwareUpdate, deviceSoftwareUpdate]) => { mergeMap(([deviceFirmwareUpdate, deviceSoftwareUpdate]) => {
let text = ''; const lines: string[] = [];
if (deviceFirmwareUpdate > 0) { if (deviceFirmwareUpdate > 0) {
text += this.translate.instant('ota-update.change-firmware', {count: deviceFirmwareUpdate}); lines.push(this.translate.instant('ota-update.change-firmware', {count: deviceFirmwareUpdate}));
} }
if (deviceSoftwareUpdate > 0) { if (deviceSoftwareUpdate > 0) {
text += text.length ? ' ' : ''; lines.push(this.translate.instant('ota-update.change-software', {count: deviceSoftwareUpdate}));
text += this.translate.instant('ota-update.change-software', {count: deviceSoftwareUpdate});
} }
return text !== '' ? this.dialogService.confirm('', text, null, this.translate.instant('common.proceed')) : of(true); return lines.length
? this.dialogService.confirm(this.translate.instant('ota-update.change-ota-setting-title'), lines.join('<br/>'), null, this.translate.instant('common.proceed'))
: of(true);
}) })
); );
} }

2
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss

@ -21,7 +21,7 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
background: #fff; background: #fff;
z-index: 5; z-index: 100;
} }
.widget-preview-section { .widget-preview-section {
position: absolute; position: absolute;

18
ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts

@ -14,7 +14,16 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { AfterViewInit, Component, ElementRef, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core'; import {
AfterViewInit,
Component,
ElementRef,
Inject,
OnInit,
SecurityContext,
SkipSelf,
ViewChild
} from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -42,6 +51,7 @@ import {
} from '@home/components/dashboard-page/states/dashboard-state-dialog.component'; } from '@home/components/dashboard-page/states/dashboard-state-dialog.component';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { Widget } from '@shared/models/widget.models'; import { Widget } from '@shared/models/widget.models';
import { DomSanitizer } from '@angular/platform-browser';
export interface ManageDashboardStatesDialogData { export interface ManageDashboardStatesDialogData {
states: {[id: string]: DashboardState }; states: {[id: string]: DashboardState };
@ -87,7 +97,8 @@ export class ManageDashboardStatesDialogComponent
private translate: TranslateService, private translate: TranslateService,
private dialogs: DialogService, private dialogs: DialogService,
private utils: UtilsService, private utils: UtilsService,
private dialog: MatDialog) { private dialog: MatDialog,
private sanitizer: DomSanitizer) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.states = this.data.states; this.states = this.data.states;
@ -148,7 +159,8 @@ export class ManageDashboardStatesDialogComponent
} }
const title = this.translate.instant('dashboard.delete-state-title'); const title = this.translate.instant('dashboard.delete-state-title');
const content = this.translate.instant('dashboard.delete-state-text', {stateName: state.name}); const content = this.translate.instant('dashboard.delete-state-text', {stateName: state.name});
this.dialogs.confirm(title, content, this.translate.instant('action.no'), const safeContent = this.sanitizer.sanitize(SecurityContext.HTML, content);
this.dialogs.confirm(title, safeContent, this.translate.instant('action.no'),
this.translate.instant('action.yes')).subscribe( this.translate.instant('action.yes')).subscribe(
(res) => { (res) => {
if (res) { if (res) {

2
ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts

@ -103,7 +103,7 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu
delete configuration.type; delete configuration.type;
} }
setTimeout(() => { setTimeout(() => {
this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: this.isAdd});
}, 0); }, 0);
} }

6
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.scss

@ -31,4 +31,10 @@
.mat-expansion-panel-header-title { .mat-expansion-panel-header-title {
margin-right: 0; margin-right: 0;
} }
&::ng-deep {
.mat-content.mat-content-hide-toggle {
margin-right: 0;
}
}
} }

2
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html

@ -49,7 +49,7 @@
</mat-checkbox> </mat-checkbox>
</div> </div>
<div class="flex max-w-10% flex-full items-center justify-center"> <div class="flex max-w-10% flex-full items-center justify-center">
<mat-checkbox class="max-w-10% flex-full" formControlName="observe" color="primary" <mat-checkbox formControlName="observe" color="primary"
matTooltip="{{ 'device-profile.lwm2m.edit-observe-select' | translate }}" matTooltip="{{ 'device-profile.lwm2m.edit-observe-select' | translate }}"
[matTooltipDisabled]="disabled || !isDisabledObserve($index)" [matTooltipDisabled]="disabled || !isDisabledObserve($index)"
matTooltipPosition="above"> matTooltipPosition="above">

1
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts

@ -146,6 +146,7 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
if (this.widgetActionFormGroup.get('actionSourceId').value === 'headerButton') { if (this.widgetActionFormGroup.get('actionSourceId').value === 'headerButton') {
this.widgetActionFormGroup.get('buttonType').enable({emitEvent: false}); this.widgetActionFormGroup.get('buttonType').enable({emitEvent: false});
this.widgetActionFormGroup.get('buttonColor').enable({emitEvent: false}); this.widgetActionFormGroup.get('buttonColor').enable({emitEvent: false});
this.widgetActionFormGroup.get('customButtonStyle').enable({emitEvent: false});
this.widgetHeaderButtonValidators(true); this.widgetHeaderButtonValidators(true);
} }
this.widgetActionFormGroup.get('actionSourceId').valueChanges.pipe( this.widgetActionFormGroup.get('actionSourceId').valueChanges.pipe(

6
ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts

@ -285,10 +285,10 @@ function getValueDec(ctx: WidgetContext, _settings: AnalogueGaugeSettings): numb
if (ctx.data && ctx.data[0]) { if (ctx.data && ctx.data[0]) {
dataKey = ctx.data[0].dataKey; dataKey = ctx.data[0].dataKey;
} }
if (dataKey && isDefined(dataKey.decimals)) { if (dataKey && isDefinedAndNotNull(dataKey.decimals)) {
return dataKey.decimals; return dataKey.decimals;
} else { } else {
return isDefinedAndNotNull(ctx.decimals) ? ctx.decimals : 0; return ctx.decimals ?? 0;
} }
} }
@ -300,6 +300,6 @@ function getUnits(ctx: WidgetContext, settings: AnalogueGaugeSettings): TbUnit {
if (dataKey?.units) { if (dataKey?.units) {
return dataKey.units; return dataKey.units;
} else { } else {
return isDefinedAndNotNull(settings.units) ? settings.units : ctx.units; return settings.units ?? ctx.units;
} }
} }

11
ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts

@ -32,7 +32,8 @@ import {
ComponentStyle, ComponentStyle,
getDataKey, getDataKey,
overlayStyle, overlayStyle,
textStyle textStyle,
ValueFormatProcessor
} from '@shared/models/widget-settings.models'; } from '@shared/models/widget-settings.models';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import { import {
@ -113,11 +114,17 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn
this.units = unitService.getTargetUnitSymbol(units); this.units = unitService.getTargetUnitSymbol(units);
this.unitConvertor = unitService.geUnitConverter(units); this.unitConvertor = unitService.geUnitConverter(units);
const valueFormat = ValueFormatProcessor.fromSettings(this.ctx.$injector, {
units,
decimals: this.decimals,
ignoreUnitSymbol: true
});
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay); this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding; this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding;
this.rangeItems = toRangeItems(this.settings.rangeColors, this.unitConvertor); this.rangeItems = toRangeItems(this.settings.rangeColors, valueFormat);
this.visibleRangeItems = this.rangeItems.filter(item => item.visible); this.visibleRangeItems = this.rangeItems.filter(item => item.visible);
this.showLegend = this.settings.showLegend && !!this.rangeItems.length; this.showLegend = this.settings.showLegend && !!this.rangeItems.length;

7
ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts

@ -22,6 +22,7 @@ import {
Font, Font,
simpleDateFormat, simpleDateFormat,
sortedColorRange, sortedColorRange,
ValueFormatProcessor,
ValueSourceType ValueSourceType
} from '@shared/models/widget-settings.models'; } from '@shared/models/widget-settings.models';
import { LegendPosition } from '@shared/models/widget.models'; import { LegendPosition } from '@shared/models/widget.models';
@ -291,21 +292,21 @@ export const rangeChartTimeSeriesKeySettings = (settings: RangeChartWidgetSettin
} }
}); });
export const toRangeItems = (colorRanges: Array<ColorRange>, convertValue: (x: number) => number): RangeItem[] => { export const toRangeItems = (colorRanges: Array<ColorRange>, valueFormat: ValueFormatProcessor): RangeItem[] => {
const rangeItems: RangeItem[] = []; const rangeItems: RangeItem[] = [];
let counter = 0; let counter = 0;
const ranges = sortedColorRange(filterIncludingColorRanges(colorRanges)).filter(r => isNumber(r.from) || isNumber(r.to)); const ranges = sortedColorRange(filterIncludingColorRanges(colorRanges)).filter(r => isNumber(r.from) || isNumber(r.to));
for (let i = 0; i < ranges.length; i++) { for (let i = 0; i < ranges.length; i++) {
const range = ranges[i]; const range = ranges[i];
let from = range.from; let from = range.from;
const to = isDefinedAndNotNull(range.to) ? convertValue(range.to) : range.to; const to = isDefinedAndNotNull(range.to) ? Number(valueFormat.format(range.to)) : range.to;
if (i > 0) { if (i > 0) {
const prevRange = ranges[i - 1]; const prevRange = ranges[i - 1];
if (isNumber(prevRange.to) && isNumber(from) && from < prevRange.to) { if (isNumber(prevRange.to) && isNumber(from) && from < prevRange.to) {
from = prevRange.to; from = prevRange.to;
} }
} }
from = isDefinedAndNotNull(from) ? convertValue(from) : from; from = isDefinedAndNotNull(from) ? Number(valueFormat.format(from)) : from;
rangeItems.push( rangeItems.push(
{ {
index: counter++, index: counter++,

9
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts

@ -17,9 +17,7 @@
import { isFunction } from '@core/utils'; import { isFunction } from '@core/utils';
import { FormattedData } from '@shared/models/widget.models'; import { FormattedData } from '@shared/models/widget.models';
import { DateFormatProcessor, DateFormatSettings, Font } from '@shared/models/widget-settings.models'; import { DateFormatProcessor, DateFormatSettings, Font } from '@shared/models/widget-settings.models';
import { import { TimeSeriesChartDataItem } from '@home/components/widget/lib/chart/time-series-chart.models';
TimeSeriesChartDataItem,
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { Renderer2, SecurityContext } from '@angular/core'; import { Renderer2, SecurityContext } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { CallbackDataParams } from 'echarts/types/dist/shared'; import { CallbackDataParams } from 'echarts/types/dist/shared';
@ -105,6 +103,9 @@ export class TimeSeriesChartTooltip {
if (!tooltipParams.items.length && !tooltipParams.comparisonItems.length) { if (!tooltipParams.items.length && !tooltipParams.comparisonItems.length) {
return null; return null;
} }
if (this.settings.tooltipHideZeroFalse && !tooltipParams.items.some(value => value.param.value[1] && value.param.value[1] !== 'false')) {
return undefined;
}
const tooltipElement: HTMLElement = this.renderer.createElement('div'); const tooltipElement: HTMLElement = this.renderer.createElement('div');
this.renderer.setStyle(tooltipElement, 'display', 'flex'); this.renderer.setStyle(tooltipElement, 'display', 'flex');
@ -132,7 +133,7 @@ export class TimeSeriesChartTooltip {
} }
let total = 0, isStacked = false; let total = 0, isStacked = false;
for (const item of items) { for (const item of items) {
if (!this.settings.tooltipHideZeroFalse || item.param.value[1]) { if (!this.settings.tooltipHideZeroFalse || (item.param.value[1] && item.param.value[1] !== 'false')) {
this.renderer.appendChild(tooltipItemsElement, this.constructTooltipSeriesElement(item)); this.renderer.appendChild(tooltipItemsElement, this.constructTooltipSeriesElement(item));
if (item.dataItem?.barRenderContext?.barStackIndex !== undefined && !isNaN(Number(item.param.value[1]))) { if (item.dataItem?.barRenderContext?.barStackIndex !== undefined && !isNaN(Number(item.param.value[1]))) {
isStacked = true; isStacked = true;

1
ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts

@ -125,6 +125,7 @@ export class TbCanvasDigitalGauge {
this.barColorProcessor = ColorProcessor.fromSettings(settings.barColor, this.ctx); this.barColorProcessor = ColorProcessor.fromSettings(settings.barColor, this.ctx);
this.valueFormat = ValueFormatProcessor.fromSettings(this.ctx.$injector, { this.valueFormat = ValueFormatProcessor.fromSettings(this.ctx.$injector, {
units: this.localSettings.units, units: this.localSettings.units,
decimals: this.localSettings.decimals,
ignoreUnitSymbol: true ignoreUnitSymbol: true
}); });

9
ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, DestroyRef } from '@angular/core'; import { Component, DestroyRef, OnInit } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
@ -31,7 +31,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
templateUrl: './device-profile-tabs.component.html', templateUrl: './device-profile-tabs.component.html',
styleUrls: [] styleUrls: []
}) })
export class DeviceProfileTabsComponent extends EntityTabsComponent<DeviceProfile> { export class DeviceProfileTabsComponent extends EntityTabsComponent<DeviceProfile> implements OnInit {
deviceTransportTypes = Object.values(DeviceTransportType); deviceTransportTypes = Object.values(DeviceTransportType);
@ -55,4 +55,9 @@ export class DeviceProfileTabsComponent extends EntityTabsComponent<DeviceProfil
}); });
} }
protected setEntity(entity: DeviceProfile) {
this.isTransportTypeChanged = false;
super.setEntity(entity);
}
} }

6
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html

@ -62,6 +62,12 @@
*ngIf="actionButtonConfigForm.get('link').hasError('required')"> *ngIf="actionButtonConfigForm.get('link').hasError('required')">
{{ 'notification.link-required' | translate }} {{ 'notification.link-required' | translate }}
</mat-error> </mat-error>
<mat-error
*ngIf="actionButtonConfigForm.get('link').hasError('maxlength')">
{{ 'notification.link-max-length' | translate :
{length: actionButtonConfigForm.get('link').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field> </mat-form-field>
<ng-template #dashboardSelector> <ng-template #dashboardSelector>
<tb-dashboard-autocomplete <tb-dashboard-autocomplete

2
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.ts

@ -84,7 +84,7 @@ export class NotificationActionButtonConfigurationComponent implements ControlVa
enabled: [false], enabled: [false],
text: [{value: '', disabled: true}, [Validators.required, Validators.maxLength(50)]], text: [{value: '', disabled: true}, [Validators.required, Validators.maxLength(50)]],
linkType: [ActionButtonLinkType.LINK], linkType: [ActionButtonLinkType.LINK],
link: [{value: '', disabled: true}, Validators.required], link: [{value: '', disabled: true}, [Validators.required, Validators.maxLength(300)]],
dashboardId: [{value: null, disabled: true}, Validators.required], dashboardId: [{value: null, disabled: true}, Validators.required],
dashboardState: [{value: null, disabled: true}], dashboardState: [{value: null, disabled: true}],
setEntityIdInState: [{value: true, disabled: true}] setEntityIdInState: [{value: true, disabled: true}]

15
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.html

@ -44,6 +44,11 @@
<mat-error *ngIf="templateConfigurationForm.get('WEB.subject').hasError('required')"> <mat-error *ngIf="templateConfigurationForm.get('WEB.subject').hasError('required')">
{{ 'notification.subject-required' | translate }} {{ 'notification.subject-required' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="templateConfigurationForm.get('WEB.subject').hasError('maxlength')">
{{'notification.subject-max-length' | translate :
{length: templateConfigurationForm.get('WEB.subject').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>notification.message</mat-label> <mat-label translate>notification.message</mat-label>
@ -56,6 +61,11 @@
<mat-error *ngIf="templateConfigurationForm.get('WEB.body').hasError('required')"> <mat-error *ngIf="templateConfigurationForm.get('WEB.body').hasError('required')">
{{ 'notification.message-required' | translate }} {{ 'notification.message-required' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="templateConfigurationForm.get('WEB.body').hasError('maxlength')">
{{ 'notification.message-max-length' | translate :
{length: templateConfigurationForm.get('WEB.body').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field> </mat-form-field>
<section formGroupName="additionalConfig" class="tb-form-panel no-padding no-border"> <section formGroupName="additionalConfig" class="tb-form-panel no-padding no-border">
<div class="tb-form-row space-between" formGroupName="icon"> <div class="tb-form-row space-between" formGroupName="icon">
@ -194,6 +204,11 @@
<mat-error *ngIf="templateConfigurationForm.get('EMAIL.subject').hasError('required')"> <mat-error *ngIf="templateConfigurationForm.get('EMAIL.subject').hasError('required')">
{{ 'notification.subject-required' | translate }} {{ 'notification.subject-required' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="templateConfigurationForm.get('EMAIL.subject').hasError('maxlength')">
{{'notification.subject-max-length' | translate :
{length: templateConfigurationForm.get('EMAIL.subject').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field> </mat-form-field>
<mat-label class="tb-title tb-required" <mat-label class="tb-title tb-required"
[class.tb-error]="(interacted || templateConfigurationForm.get('EMAIL.body').touched) && templateConfigurationForm.get('EMAIL.body').hasError('required')" [class.tb-error]="(interacted || templateConfigurationForm.get('EMAIL.body').touched) && templateConfigurationForm.get('EMAIL.body').hasError('required')"

6
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts

@ -226,8 +226,8 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
switch (deliveryMethod) { switch (deliveryMethod) {
case NotificationDeliveryMethod.WEB: case NotificationDeliveryMethod.WEB:
deliveryMethodForm = this.fb.group({ deliveryMethodForm = this.fb.group({
subject: ['', Validators.required], subject: ['', [Validators.required, Validators.maxLength(150)]],
body: ['', Validators.required], body: ['', [Validators.required, Validators.maxLength(250)]],
additionalConfig: this.fb.group({ additionalConfig: this.fb.group({
icon: this.fb.group({ icon: this.fb.group({
enabled: [false], enabled: [false],
@ -252,7 +252,7 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
break; break;
case NotificationDeliveryMethod.EMAIL: case NotificationDeliveryMethod.EMAIL:
deliveryMethodForm = this.fb.group({ deliveryMethodForm = this.fb.group({
subject: ['', Validators.required], subject: ['', [Validators.required, Validators.maxLength(250)]],
body: ['', Validators.required] body: ['', Validators.required]
}); });
break; break;

2
ui-ngx/src/assets/dashboard/api_usage.json

@ -7885,6 +7885,7 @@
}, },
"tooltipDateColor": "rgba(0, 0, 0, 0.76)", "tooltipDateColor": "rgba(0, 0, 0, 0.76)",
"tooltipDateInterval": true, "tooltipDateInterval": true,
"tooltipHideZeroFalse": true,
"tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)",
"tooltipBackgroundBlur": 4, "tooltipBackgroundBlur": 4,
"animation": { "animation": {
@ -8293,6 +8294,7 @@
}, },
"tooltipDateColor": "rgba(0, 0, 0, 0.76)", "tooltipDateColor": "rgba(0, 0, 0, 0.76)",
"tooltipDateInterval": true, "tooltipDateInterval": true,
"tooltipHideZeroFalse": true,
"tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)",
"tooltipBackgroundBlur": 4, "tooltipBackgroundBlur": 4,
"animation": { "animation": {

6
ui-ngx/src/assets/help/en_US/notification/resources_shortage.md

@ -9,8 +9,10 @@ See the available types and parameters below:
Available template parameters: Available template parameters:
* `resource` - the resource name; * `resource` - the resource name (e.g., "CPU", "RAM", "STORAGE");
* `usage` - the resource usage value; * `usage` - the current usage value of the resource;
* `serviceId` - the service id (convenient in cluster setup);
* `serviceType` - the service type (convenient in cluster setup);
Parameter names must be wrapped using `${...}`. For example: `${resource}`. Parameter names must be wrapped using `${...}`. For example: `${resource}`.
You may also modify the value of the parameter with one of the suffixes: You may also modify the value of the parameter with one of the suffixes:

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

@ -3963,6 +3963,7 @@
"input-fields-support-templatization": "Input fields support templatization.", "input-fields-support-templatization": "Input fields support templatization.",
"link": "Link", "link": "Link",
"link-required": "Link is required", "link-required": "Link is required",
"link-max-length": "Link should be less than or equal to {{ length }} characters",
"link-type": { "link-type": {
"dashboard": "Open dashboard", "dashboard": "Open dashboard",
"link": "Open URL link" "link": "Open URL link"
@ -4167,6 +4168,7 @@
"checksum-copied-message": "Package checksum has been copied to clipboard", "checksum-copied-message": "Package checksum has been copied to clipboard",
"change-firmware": "Change of the firmware may cause update of { count, plural, =1 {1 device} other {# devices} }.", "change-firmware": "Change of the firmware may cause update of { count, plural, =1 {1 device} other {# devices} }.",
"change-software": "Change of the software may cause update of { count, plural, =1 {1 device} other {# devices} }.", "change-software": "Change of the software may cause update of { count, plural, =1 {1 device} other {# devices} }.",
"change-ota-setting-title": "Are you sure you want to change OTA settings?",
"chose-compatible-device-profile": "The uploaded package will be available only for devices with the chosen profile.", "chose-compatible-device-profile": "The uploaded package will be available only for devices with the chosen profile.",
"chose-firmware-distributed-device": "Choose firmware that will be distributed to the devices", "chose-firmware-distributed-device": "Choose firmware that will be distributed to the devices",
"chose-software-distributed-device": "Choose software that will be distributed to the devices", "chose-software-distributed-device": "Choose software that will be distributed to the devices",

24
ui-ngx/src/scss/animations.scss

@ -16,15 +16,23 @@
@keyframes tbMoveFromTopFade { @keyframes tbMoveFromTopFade {
from { from {
opacity: 0; opacity: 0;
transform: translate(0, -100%); transform: translate(0, -100%);
} }
to {
opacity: 1;
transform: translate(0, 0);
}
} }
@keyframes tbMoveToTopFade { @keyframes tbMoveToTopFade {
from {
opacity: 1;
transform: translate(0, 0);
}
to { to {
opacity: 0; opacity: 0;
transform: translate(0, -100%); transform: translate(0, -100%);
} }
} }
@ -32,15 +40,23 @@
@keyframes tbMoveFromBottomFade { @keyframes tbMoveFromBottomFade {
from { from {
opacity: 0; opacity: 0;
transform: translate(0, 100%); transform: translate(0, 100%);
} }
to {
opacity: 1;
transform: translate(0, 0);
}
} }
@keyframes tbMoveToBottomFade { @keyframes tbMoveToBottomFade {
from {
opacity: 1;
transform: translate(0, 0);
}
to { to {
opacity: 0; opacity: 0;
transform: translate(0, 150%); transform: translate(0, 150%);
} }
} }

Loading…
Cancel
Save