Browse Source

Merge branch 'rc'

pull/13596/head
Igor Kulikov 1 year ago
parent
commit
550109080e
  1. 2
      application/src/main/data/json/demo/dashboards/rule_engine_statistics.json
  2. 13
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java
  3. 33
      application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java
  4. 7
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java
  5. 25
      application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java
  6. 4
      application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
  7. 39
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java
  8. 6
      application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java
  9. 40
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  10. 1
      common/data/src/main/java/org/thingsboard/server/common/data/SystemInfoData.java
  11. 7
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsState.java
  12. 11
      common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java
  13. 10
      common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java
  14. 1
      common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
  15. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/ResourcesShortageNotificationInfo.java
  16. 9
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java
  17. 9
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java
  18. 9
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java
  19. 11
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTrigger.java
  20. 8
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java
  21. 8
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java
  22. 1
      common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java
  23. 1
      common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java
  24. 51
      common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java
  25. 3
      common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java
  26. 2
      common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java
  27. 10
      dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java
  28. 3
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
  29. 8
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  30. 11
      dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java
  31. 19
      dao/src/main/java/org/thingsboard/server/dao/util/TenantRateLimitException.java
  32. 16
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java
  33. 19
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java
  34. 20
      netty-mqtt/src/test/java/org/thingsboard/mqtt/MqttClientTest.java
  35. 11
      ui-ngx/src/app/core/http/ota-package.service.ts
  36. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss
  37. 18
      ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts
  38. 2
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts
  39. 6
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.scss
  40. 2
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html
  41. 1
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
  42. 6
      ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts
  43. 11
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts
  44. 7
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts
  45. 9
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts
  46. 1
      ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts
  47. 9
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts
  48. 6
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html
  49. 2
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.ts
  50. 15
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.html
  51. 6
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts
  52. 2
      ui-ngx/src/assets/dashboard/api_usage.json
  53. 6
      ui-ngx/src/assets/help/en_US/notification/resources_shortage.md
  54. 2
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  55. 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": {

13
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java

@ -16,13 +16,16 @@
package org.thingsboard.server.service.cf.ctx.state; package org.thingsboard.server.service.cf.ctx.state;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.common.util.ProtoUtils;
@ -90,7 +93,15 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
@Override @Override
public TbelCfArg toTbelCfArg() { public TbelCfArg toTbelCfArg() {
return new TbelCfSingleValueArg(ts, kvEntryValue.getValue()); Object value = kvEntryValue.getValue();
if (kvEntryValue instanceof JsonDataEntry) {
try {
value = JacksonUtil.readValue(kvEntryValue.getValueAsString(), new TypeReference<>() {
});
} catch (Exception e) {
}
}
return new TbelCfSingleValueArg(ts, value);
} }
@Override @Override

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) {

4
application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java

@ -36,6 +36,7 @@ import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.exception.RateLimitExceededException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityIdFactory;
@ -52,7 +53,6 @@ import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.util.TenantRateLimitException;
import org.thingsboard.server.exception.UnauthorizedException; import org.thingsboard.server.exception.UnauthorizedException;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -742,7 +742,7 @@ public class DefaultWebSocketService implements WebSocketService {
@Override @Override
public void onFailure(Throwable e) { public void onFailure(Throwable e) {
if (e instanceof TenantRateLimitException || e.getCause() instanceof TenantRateLimitException) { if (e instanceof RateLimitExceededException || e.getCause() instanceof RateLimitExceededException) {
log.trace("[{}] Tenant rate limit detected for subscription: [{}]:{}", sessionRef.getSecurityCtx().getTenantId(), entityId, cmd); log.trace("[{}] Tenant rate limit detected for subscription: [{}]:{}", sessionRef.getSecurityCtx().getTenantId(), entityId, cmd);
} else { } else {
log.info(FAILED_TO_FETCH_DATA, e); log.info(FAILED_TO_FETCH_DATA, e);

39
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java

@ -17,8 +17,15 @@ package org.thingsboard.server.service.cf.ctx.state;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -73,4 +80,34 @@ public class SingleValueArgumentEntryTest {
void testUpdateEntryWhenValueWasNotChanged() { void testUpdateEntryWhenValueWasNotChanged() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 364L))).isTrue(); assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 364L))).isTrue();
} }
}
@Test
void testToTbelCfArgWhenJsonIsObject() {
entry = new SingleValueArgumentEntry(ts, new JsonDataEntry("key", "{\"test\": 10}"), 370L);
TbelCfArg tbelCfArg = entry.toTbelCfArg();
assertThat(tbelCfArg).isNotNull();
assertThat(tbelCfArg).isInstanceOf(TbelCfSingleValueArg.class);
TbelCfSingleValueArg singleValueArg = (TbelCfSingleValueArg) tbelCfArg;
assertThat(singleValueArg.getValue()).isInstanceOf(Map.class);
Map<String, Integer> expectedMap = Map.of("test", 10);
assertThat(singleValueArg.getValue()).isEqualTo(expectedMap);
}
@Test
void testToTbelCfArgWhenJsonIsArray() {
entry = new SingleValueArgumentEntry(ts, new JsonDataEntry("key", "[{\"test\": 10}, {\"test2\": 20}]"), 371L);
TbelCfArg tbelCfArg = entry.toTbelCfArg();
assertThat(tbelCfArg).isNotNull();
assertThat(tbelCfArg).isInstanceOf(TbelCfSingleValueArg.class);
TbelCfSingleValueArg singleValueArg = (TbelCfSingleValueArg) tbelCfArg;
assertThat(singleValueArg.getValue()).isInstanceOf(List.class);
List<Map<String, Integer>> expectedList = new ArrayList<>();
expectedList.add(Map.of("test", 10));
expectedList.add(Map.of("test2", 20));
assertThat(singleValueArg.getValue()).isEqualTo(expectedList);
}
}

6
application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java

@ -89,7 +89,7 @@ public class JobManagerTest extends AbstractControllerTest {
@Test @Test
public void testSubmitJob_allTasksSuccessful() { public void testSubmitJob_allTasksSuccessful() {
int tasksCount = 5; int tasksCount = 7;
JobId jobId = submitJob(DummyJobConfiguration.builder() JobId jobId = submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(tasksCount) .successfulTasksCount(tasksCount)
.taskProcessingTimeMs(1000) .taskProcessingTimeMs(1000)
@ -154,10 +154,10 @@ public class JobManagerTest extends AbstractControllerTest {
@Test @Test
public void testCancelJob_whileRunning() throws Exception { public void testCancelJob_whileRunning() throws Exception {
int tasksCount = 100; int tasksCount = 200;
JobId jobId = submitJob(DummyJobConfiguration.builder() JobId jobId = submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(tasksCount) .successfulTasksCount(tasksCount)
.taskProcessingTimeMs(100) .taskProcessingTimeMs(50)
.build()).getId(); .build()).getId();
Thread.sleep(500); Thread.sleep(500);

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.")

7
common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsState.java

@ -20,7 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.apache.commons.lang3.BooleanUtils;
import static org.apache.commons.lang3.BooleanUtils.toBooleanDefaultIfNull;
@Getter @Getter
@NoArgsConstructor @NoArgsConstructor
@ -34,14 +35,14 @@ public class EdqsState {
private EdqsApiMode apiMode; private EdqsApiMode apiMode;
public boolean updateEdqsReady(boolean ready) { public boolean updateEdqsReady(boolean ready) {
boolean changed = BooleanUtils.toBooleanDefaultIfNull(this.edqsReady, false) != ready; boolean changed = toBooleanDefaultIfNull(this.edqsReady, false) != ready;
this.edqsReady = ready; this.edqsReady = ready;
return changed; return changed;
} }
@JsonIgnore @JsonIgnore
public boolean isApiReady() { public boolean isApiReady() {
return edqsReady && syncStatus == EdqsSyncStatus.FINISHED; return toBooleanDefaultIfNull(edqsReady, false) && syncStatus == EdqsSyncStatus.FINISHED;
} }
@JsonIgnore @JsonIgnore

11
common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.common.data.job.task; package org.thingsboard.server.common.data.job.task;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@ -25,22 +26,25 @@ import org.thingsboard.server.common.data.job.JobType;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@NoArgsConstructor @NoArgsConstructor
@SuperBuilder
@ToString(callSuper = true) @ToString(callSuper = true)
public class DummyTaskResult extends TaskResult { public class DummyTaskResult extends TaskResult {
private DummyTaskFailure failure; private DummyTaskFailure failure;
@Builder
private DummyTaskResult(boolean success, boolean discarded, DummyTaskFailure failure) {
super(success, discarded);
this.failure = failure;
}
public static DummyTaskResult success(DummyTask task) { public static DummyTaskResult success(DummyTask task) {
return DummyTaskResult.builder() return DummyTaskResult.builder()
.key(task.getKey())
.success(true) .success(true)
.build(); .build();
} }
public static DummyTaskResult failed(DummyTask task, Throwable error) { public static DummyTaskResult failed(DummyTask task, Throwable error) {
return DummyTaskResult.builder() return DummyTaskResult.builder()
.key(task.getKey())
.failure(DummyTaskFailure.builder() .failure(DummyTaskFailure.builder()
.error(error.getMessage()) .error(error.getMessage())
.number(task.getNumber()) .number(task.getNumber())
@ -51,7 +55,6 @@ public class DummyTaskResult extends TaskResult {
public static DummyTaskResult discarded(DummyTask task) { public static DummyTaskResult discarded(DummyTask task) {
return DummyTaskResult.builder() return DummyTaskResult.builder()
.key(task.getKey())
.discarded(true) .discarded(true)
.build(); .build();
} }

10
common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java

@ -20,16 +20,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type; import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.JobType;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
@SuperBuilder
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType")
@JsonSubTypes({ @JsonSubTypes({
@ -40,6 +36,12 @@ public abstract class TaskResult {
private String key; private String key;
private boolean success; private boolean success;
private boolean discarded; private boolean discarded;
private long finishTs;
protected TaskResult(boolean success, boolean discarded) {
this.success = success;
this.discarded = discarded;
}
@JsonIgnore @JsonIgnore
public abstract JobType getJobType(); public abstract JobType getJobType();

1
common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java

@ -43,6 +43,7 @@ public enum LimitedApi {
RateLimitUtil.merge( RateLimitUtil.merge(
DefaultTenantProfileConfiguration::getCassandraWriteQueryTenantCoreRateLimits, DefaultTenantProfileConfiguration::getCassandraWriteQueryTenantCoreRateLimits,
DefaultTenantProfileConfiguration::getCassandraWriteQueryTenantRuleEngineRateLimits), "Monolith telemetry Cassandra write queries", true), DefaultTenantProfileConfiguration::getCassandraWriteQueryTenantRuleEngineRateLimits), "Monolith telemetry Cassandra write queries", true),
CASSANDRA_QUERIES(null, true), // left for backward compatibility with RateLimitsNotificationInfo
EDGE_EVENTS(DefaultTenantProfileConfiguration::getEdgeEventRateLimits, "Edge events", true), EDGE_EVENTS(DefaultTenantProfileConfiguration::getEdgeEventRateLimits, "Edge events", true),
EDGE_EVENTS_PER_EDGE(DefaultTenantProfileConfiguration::getEdgeEventRateLimitsPerEdge, "Edge events per edge", false), EDGE_EVENTS_PER_EDGE(DefaultTenantProfileConfiguration::getEdgeEventRateLimitsPerEdge, "Edge events per edge", false),
EDGE_UPLINK_MESSAGES(DefaultTenantProfileConfiguration::getEdgeUplinkMessagesRateLimits, "Edge uplink messages", true), EDGE_UPLINK_MESSAGES(DefaultTenantProfileConfiguration::getEdgeUplinkMessagesRateLimits, "Edge uplink messages", true),

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

1
common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java

@ -277,6 +277,7 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>,
eventConsumer.awaitStop(); eventConsumer.awaitStop();
responseTemplate.stop(); responseTemplate.stop();
stateService.stop(); stateService.stop();
versionsStore.shutdown();
} }
} }

1
common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java

@ -224,6 +224,7 @@ public class KafkaEdqsStateService implements EdqsStateService {
stateConsumer.awaitStop(); stateConsumer.awaitStop();
eventsToBackupConsumer.stop(); eventsToBackupConsumer.stop();
stateProducer.stop(); stateProducer.stop();
versionsStore.shutdown();
} }
} }

51
common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java

@ -15,31 +15,35 @@
*/ */
package org.thingsboard.server.edqs.util; package org.thingsboard.server.edqs.util;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.edqs.EdqsObjectKey; import org.thingsboard.server.common.data.edqs.EdqsObjectKey;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j @Slf4j
public class VersionsStore { public class VersionsStore {
private final Cache<EdqsObjectKey, Long> versions; private final ConcurrentMap<EdqsObjectKey, TimedValue> versions = new ConcurrentHashMap<>();
private final long expirationMillis;
private final ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor();
public VersionsStore(int ttlMinutes) { public VersionsStore(int ttlMinutes) {
this.versions = Caffeine.newBuilder() this.expirationMillis = TimeUnit.MINUTES.toMillis(ttlMinutes);
.expireAfterWrite(ttlMinutes, TimeUnit.MINUTES) startCleanupTask();
.build();
} }
public boolean isNew(EdqsObjectKey key, Long version) { public boolean isNew(EdqsObjectKey key, Long version) {
AtomicBoolean isNew = new AtomicBoolean(false); AtomicBoolean isNew = new AtomicBoolean(false);
versions.asMap().compute(key, (k, prevVersion) -> { versions.compute(key, (k, prevVersion) -> {
if (prevVersion == null || prevVersion <= version) { if (prevVersion == null || prevVersion.value <= version) {
isNew.set(true); isNew.set(true);
return version; return new TimedValue(version);
} else { } else {
log.debug("[{}] Version {} is outdated, the latest is {}", key, version, prevVersion); log.debug("[{}] Version {} is outdated, the latest is {}", key, version, prevVersion);
return prevVersion; return prevVersion;
@ -48,4 +52,33 @@ public class VersionsStore {
return isNew.get(); return isNew.get();
} }
private void startCleanupTask() {
cleaner.scheduleAtFixedRate(() -> {
try {
long now = System.currentTimeMillis();
for (Map.Entry<EdqsObjectKey, TimedValue> entry : versions.entrySet()) {
if (now - entry.getValue().lastUpdated > expirationMillis) {
versions.remove(entry.getKey(), entry.getValue());
}
}
} catch (Exception e) {
log.error("Cleanup task failed", e);
}
}, expirationMillis, expirationMillis, TimeUnit.MILLISECONDS);
}
public void shutdown() {
cleaner.shutdown();
}
private static class TimedValue {
private final long lastUpdated;
private final long value;
public TimedValue(long value) {
this.value = value;
this.lastUpdated = System.currentTimeMillis();
}
}
} }

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
common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java

@ -232,6 +232,8 @@ public abstract class TaskProcessor<T extends Task<R>, R extends TaskResult> {
} }
private void reportTaskResult(T task, R result) { private void reportTaskResult(T task, R result) {
result.setKey(task.getKey());
result.setFinishTs(System.currentTimeMillis());
statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result); statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result);
} }

10
dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java

@ -69,7 +69,6 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
job.setStatus(QUEUED); job.setStatus(QUEUED);
} else { } else {
job.setStatus(PENDING); job.setStatus(PENDING);
job.getResult().setStartTs(System.currentTimeMillis());
} }
return saveJob(tenantId, job, true, null); return saveJob(tenantId, job, true, null);
} }
@ -125,6 +124,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
} }
boolean publishEvent = false; boolean publishEvent = false;
long lastFinishTs = 0;
for (TaskResult taskResult : jobStats.getTaskResults()) { for (TaskResult taskResult : jobStats.getTaskResults()) {
if (!taskResult.getKey().equals(job.getConfiguration().getTasksKey())) { if (!taskResult.getKey().equals(job.getConfiguration().getTasksKey())) {
log.debug("Ignoring task result {} with outdated key {}", taskResult, job.getConfiguration().getTasksKey()); log.debug("Ignoring task result {} with outdated key {}", taskResult, job.getConfiguration().getTasksKey());
@ -140,6 +140,9 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
publishEvent = true; publishEvent = true;
} }
} }
if (taskResult.getFinishTs() > lastFinishTs) {
lastFinishTs = taskResult.getFinishTs();
}
} }
if (job.getStatus() == RUNNING) { if (job.getStatus() == RUNNING) {
@ -153,7 +156,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
job.setStatus(COMPLETED); job.setStatus(COMPLETED);
publishEvent = true; publishEvent = true;
} }
result.setFinishTs(System.currentTimeMillis()); result.setFinishTs(lastFinishTs);
job.getConfiguration().setToReprocess(null); job.getConfiguration().setToReprocess(null);
} }
} }
@ -166,6 +169,9 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
if (!Job.SUPPORTED_ENTITY_TYPES.contains(job.getEntityId().getEntityType())) { if (!Job.SUPPORTED_ENTITY_TYPES.contains(job.getEntityId().getEntityType())) {
throw new IllegalArgumentException("Unsupported entity type " + job.getEntityId().getEntityType()); throw new IllegalArgumentException("Unsupported entity type " + job.getEntityId().getEntityType());
} }
if (job.getStatus() == PENDING) {
job.getResult().setStartTs(System.currentTimeMillis());
}
job = jobDao.save(tenantId, job); job = jobDao.save(tenantId, job);
if (publishEvent) { if (publishEvent) {

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

@ -33,7 +33,6 @@ import org.thingsboard.server.common.data.notification.rule.DefaultNotificationR
import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig; import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig;
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.NotificationRuleConfig; import org.thingsboard.server.common.data.notification.rule.NotificationRuleConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.ResourcesShortageTrigger.Resource;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig;
@ -377,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()

8
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java

@ -205,8 +205,8 @@ public class BaseTimeseriesService implements TimeseriesService {
ListenableFuture<Integer> dpsFuture = saveTs ? Futures.transform(Futures.allAsList(tsFutures), SUM_ALL_INTEGERS, MoreExecutors.directExecutor()) : Futures.immediateFuture(0); ListenableFuture<Integer> dpsFuture = saveTs ? Futures.transform(Futures.allAsList(tsFutures), SUM_ALL_INTEGERS, MoreExecutors.directExecutor()) : Futures.immediateFuture(0);
ListenableFuture<List<Long>> versionsFuture = saveLatest ? Futures.allAsList(latestFutures) : Futures.immediateFuture(null); ListenableFuture<List<Long>> versionsFuture = saveLatest ? Futures.allAsList(latestFutures) : Futures.immediateFuture(null);
return Futures.whenAllComplete(dpsFuture, versionsFuture).call(() -> { return Futures.whenAllComplete(dpsFuture, versionsFuture).call(() -> {
Integer dataPoints = Futures.getUnchecked(dpsFuture); Integer dataPoints = dpsFuture.get();
List<Long> versions = Futures.getUnchecked(versionsFuture); List<Long> versions = versionsFuture.get();
return TimeseriesSaveResult.of(dataPoints, versions); return TimeseriesSaveResult.of(dataPoints, versions);
}, MoreExecutors.directExecutor()); }, MoreExecutors.directExecutor());
} }
@ -298,13 +298,13 @@ public class BaseTimeseriesService implements TimeseriesService {
long interval = query.getInterval(); long interval = query.getInterval();
if (interval < 1) { if (interval < 1) {
throw new IncorrectParameterException("Invalid TsKvQuery: 'interval' must be greater than 0, but got " + interval + throw new IncorrectParameterException("Invalid TsKvQuery: 'interval' must be greater than 0, but got " + interval +
". Please check your query parameters and ensure 'endTs' is greater than 'startTs' or increase 'interval'."); ". Please check your query parameters and ensure 'endTs' is greater than 'startTs' or increase 'interval'.");
} }
long step = Math.max(interval, 1000); long step = Math.max(interval, 1000);
long intervalCounts = (query.getEndTs() - query.getStartTs()) / step; long intervalCounts = (query.getEndTs() - query.getStartTs()) / step;
if (intervalCounts > maxTsIntervals || intervalCounts < 0) { if (intervalCounts > maxTsIntervals || intervalCounts < 0) {
throw new IncorrectParameterException("Incorrect TsKvQuery. Number of intervals is to high - " + intervalCounts + ". " + throw new IncorrectParameterException("Incorrect TsKvQuery. Number of intervals is to high - " + intervalCounts + ". " +
"Please increase 'interval' parameter for your query or reduce the time range of the query."); "Please increase 'interval' parameter for your query or reduce the time range of the query.");
} }
} }
} }

11
dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java

@ -32,6 +32,7 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.exception.RateLimitExceededException;
import org.thingsboard.server.common.data.id.TenantId; 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.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
@ -66,7 +67,7 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
private final long maxWaitTime; private final long maxWaitTime;
private final long pollMs; private final long pollMs;
private final String bufferName; private final String bufferName;
private final BlockingQueue<AsyncTaskContext<T, V>> queue; private final BlockingQueue<AsyncTaskContext<T, V>> queue;
private final ExecutorService dispatcherExecutor; private final ExecutorService dispatcherExecutor;
private final ExecutorService callbackExecutor; private final ExecutorService callbackExecutor;
@ -124,7 +125,7 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
if (!rateLimitService.checkRateLimit(myLimitedApi, tenantId, tenantId, true)) { if (!rateLimitService.checkRateLimit(myLimitedApi, tenantId, tenantId, true)) {
stats.incrementRateLimitedTenant(tenantId); stats.incrementRateLimitedTenant(tenantId);
stats.getTotalRateLimited().increment(); stats.getTotalRateLimited().increment();
settableFuture.setException(new TenantRateLimitException()); settableFuture.setException(new RateLimitExceededException(myLimitedApi));
perTenantLimitReached = true; perTenantLimitReached = true;
} }
} else if (tenantId == null) { } else if (tenantId == null) {
@ -299,9 +300,9 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
.count(); .count();
if (queueSize > 0 if (queueSize > 0
|| rateLimitedTenantsCount > 0 || rateLimitedTenantsCount > 0
|| concurrencyLevel.get() > 0 || concurrencyLevel.get() > 0
|| stats.getStatsCounters().stream().anyMatch(counter -> counter.get() > 0) || stats.getStatsCounters().stream().anyMatch(counter -> counter.get() > 0)
) { ) {
StringBuilder statsBuilder = new StringBuilder(); StringBuilder statsBuilder = new StringBuilder();

19
dao/src/main/java/org/thingsboard/server/dao/util/TenantRateLimitException.java

@ -1,19 +0,0 @@
/**
* 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.dao.util;
public class TenantRateLimitException extends Exception {
}

16
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java

@ -556,22 +556,6 @@ public class MqttClientTest extends AbstractContainerTest {
assertThat(provisionResponse.get("status").asText()).isEqualTo("NOT_FOUND"); assertThat(provisionResponse.get("status").asText()).isEqualTo("NOT_FOUND");
} }
@Test
public void regularDisconnect() throws Exception {
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
MqttMessageListener listener = new MqttMessageListener();
MqttClient mqttClient = getMqttClient(deviceCredentials, listener, MqttVersion.MQTT_5);
final List<Byte> returnCodeByteValue = new ArrayList<>();
MqttClientCallback callbackForDisconnectWithReturnCode = getCallbackWrapperForDisconnectWithReturnCode(returnCodeByteValue);
mqttClient.setCallback(callbackForDisconnectWithReturnCode);
mqttClient.disconnect();
Thread.sleep(1000);
assertThat(returnCodeByteValue.size()).isEqualTo(1);
MqttReasonCodes.Disconnect returnCode = MqttReasonCodes.Disconnect.valueOf(returnCodeByteValue.get(0));
assertThat(returnCode).isEqualTo(MqttReasonCodes.Disconnect.NORMAL_DISCONNECT);
}
@Test @Test
public void clientSessionTakenOverDisconnect() throws Exception { public void clientSessionTakenOverDisconnect() throws Exception {
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId()); DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());

19
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java

@ -96,6 +96,8 @@ final class MqttClientImpl implements MqttClient {
private final ListeningExecutor handlerExecutor; private final ListeningExecutor handlerExecutor;
private final static int DISCONNECT_FALLBACK_DELAY_SECS = 1;
/** /**
* Construct the MqttClientImpl with default config * Construct the MqttClientImpl with default config
*/ */
@ -456,16 +458,25 @@ final class MqttClientImpl implements MqttClient {
@Override @Override
public void disconnect() { public void disconnect() {
if (disconnected) {
return;
}
log.trace("[{}] Disconnecting from server", channel != null ? channel.id() : "UNKNOWN"); log.trace("[{}] Disconnecting from server", channel != null ? channel.id() : "UNKNOWN");
disconnected = true;
if (this.channel != null) { if (this.channel != null) {
MqttMessage message = new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0)); MqttMessage message = new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0));
ChannelFuture channelFuture = this.sendAndFlushPacket(message);
sendAndFlushPacket(message).addListener((ChannelFutureListener) future -> {
future.channel().close();
disconnected = true;
});
eventLoop.schedule(() -> { eventLoop.schedule(() -> {
if (!channelFuture.isDone()) { if (channel.isOpen()) {
log.trace("[{}] Channel still open after {} second; forcing close now", channel.id(), DISCONNECT_FALLBACK_DELAY_SECS);
this.channel.close(); this.channel.close();
disconnected = true;
} }
}, 500, TimeUnit.MILLISECONDS); }, DISCONNECT_FALLBACK_DELAY_SECS, TimeUnit.SECONDS);
} }
} }

20
netty-mqtt/src/test/java/org/thingsboard/mqtt/MqttClientTest.java

@ -119,6 +119,26 @@ class MqttClientTest {
assertThat(client.isConnected()).isTrue(); assertThat(client.isConnected()).isTrue();
} }
@Test
void testDisconnectFromBroker() {
// GIVEN
var clientConfig = new MqttClientConfig();
clientConfig.setOwnerId("Test[Disconnect]");
clientConfig.setClientId("disconnect");
client = MqttClient.create(clientConfig, null, handlerExecutor);
connect(broker.getHost(), broker.getMqttPort());
// WHEN
client.disconnect();
// THEN
Awaitility.await("waiting for client to disconnect")
.atMost(Duration.ofSeconds(5))
.untilAsserted(() -> assertThat(client.isConnected()).isFalse());
}
@Test @Test
void testDisconnectDueToKeepAliveIfNoActivity() { void testDisconnectDueToKeepAliveIfNoActivity() {
// GIVEN // GIVEN

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';
@ -104,6 +102,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');
@ -130,7 +131,7 @@ export class TimeSeriesChartTooltip {
this.renderer.appendChild(tooltipItemsElement, this.constructTooltipDateElement(items[0].param, interval)); this.renderer.appendChild(tooltipItemsElement, this.constructTooltipDateElement(items[0].param, interval));
} }
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));
} }
} }

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