Browse Source

Merge branch 'rc' into activity-fix

# Conflicts:
#	application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
pull/13541/head
Dmytro Skarzhynets 1 year ago
parent
commit
ccc2e6e635
No known key found for this signature in database GPG Key ID: 2B51652F224037DF
  1. 2
      application/src/main/data/json/demo/dashboards/rule_engine_statistics.json
  2. 6
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  3. 33
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  4. 58
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  5. 90
      application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java
  6. 33
      application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java
  7. 7
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java
  8. 25
      application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.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. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/ResourcesShortageNotificationInfo.java
  13. 9
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java
  14. 9
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java
  15. 9
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java
  16. 11
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTrigger.java
  17. 8
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java
  18. 8
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java
  19. 3
      common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java
  20. 2
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
  21. 11
      ui-ngx/src/app/core/http/ota-package.service.ts
  22. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss
  23. 18
      ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts
  24. 2
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts
  25. 6
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.scss
  26. 2
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html
  27. 1
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
  28. 6
      ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts
  29. 11
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts
  30. 7
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts
  31. 9
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts
  32. 1
      ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts
  33. 9
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts
  34. 6
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html
  35. 2
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.ts
  36. 15
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.html
  37. 6
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts
  38. 2
      ui-ngx/src/assets/dashboard/api_usage.json
  39. 6
      ui-ngx/src/assets/help/en_US/notification/resources_shortage.md
  40. 2
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  41. 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": {

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

@ -199,6 +199,7 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
@ -420,7 +421,7 @@ public abstract class BaseController {
return handleException(exception, true); return handleException(exception, true);
} }
private ThingsboardException handleException(Exception exception, boolean logException) { private ThingsboardException handleException(Throwable exception, boolean logException) {
if (logException && logControllerErrorStackTrace) { if (logException && logControllerErrorStackTrace) {
try { try {
SecurityUser user = getCurrentUser(); SecurityUser user = getCurrentUser();
@ -431,6 +432,9 @@ public abstract class BaseController {
} }
Throwable cause = exception.getCause(); Throwable cause = exception.getCause();
if (exception instanceof ExecutionException) {
exception = cause;
}
if (exception instanceof ThingsboardException) { if (exception instanceof ThingsboardException) {
return (ThingsboardException) exception; return (ThingsboardException) exception;
} else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException } else if (exception instanceof IllegalArgumentException || exception instanceof IncorrectParameterException

33
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -69,7 +69,9 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@ -193,6 +195,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler"); this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler");
this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler"); this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler");
this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service"); this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service");
this.executorService.scheduleAtFixedRate(this::destroyKafkaSessionIfDisconnectedAndConsumerActive, 60, 60, TimeUnit.SECONDS);
log.info("Edge RPC service initialized!"); log.info("Edge RPC service initialized!");
} }
@ -262,6 +265,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
@Override @Override
public void updateEdge(TenantId tenantId, Edge edge) { public void updateEdge(TenantId tenantId, Edge edge) {
if (edge == null) {
log.warn("[{}] Edge is null - edge is removed and outdated notification is in process!", tenantId);
return;
}
EdgeGrpcSession session = sessions.get(edge.getId()); EdgeGrpcSession session = sessions.get(edge.getId());
if (session != null && session.isConnected()) { if (session != null && session.isConnected()) {
log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId()); log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId());
@ -459,11 +466,14 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void processEdgeEventMigrationIfNeeded(EdgeGrpcSession session, EdgeId edgeId) throws Exception { private void processEdgeEventMigrationIfNeeded(EdgeGrpcSession session, EdgeId edgeId) throws Exception {
boolean isMigrationProcessed = edgeEventsMigrationProcessed.getOrDefault(edgeId, Boolean.FALSE); boolean isMigrationProcessed = edgeEventsMigrationProcessed.getOrDefault(edgeId, Boolean.FALSE);
if (!isMigrationProcessed) { if (!isMigrationProcessed) {
log.info("Starting edge event migration for edge [{}]", edgeId.getId());
Boolean eventsExist = session.migrateEdgeEvents().get(); Boolean eventsExist = session.migrateEdgeEvents().get();
if (Boolean.TRUE.equals(eventsExist)) { if (Boolean.TRUE.equals(eventsExist)) {
log.info("Migration still in progress for edge [{}]", edgeId.getId());
sessionNewEvents.put(edgeId, true); sessionNewEvents.put(edgeId, true);
scheduleEdgeEventsCheck(session); scheduleEdgeEventsCheck(session);
} else if (Boolean.FALSE.equals(eventsExist)) { } else if (Boolean.FALSE.equals(eventsExist)) {
log.info("Migration completed for edge [{}]", edgeId.getId());
edgeEventsMigrationProcessed.put(edgeId, true); edgeEventsMigrationProcessed.put(edgeId, true);
} }
} }
@ -610,4 +620,27 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
} }
} }
private void destroyKafkaSessionIfDisconnectedAndConsumerActive() {
try {
List<EdgeId> toRemove = new ArrayList<>();
for (EdgeGrpcSession session : sessions.values()) {
if (session instanceof KafkaEdgeGrpcSession kafkaSession &&
!kafkaSession.isConnected() &&
kafkaSession.getConsumer() != null &&
kafkaSession.getConsumer().getConsumer() != null &&
!kafkaSession.getConsumer().getConsumer().isStopped()) {
toRemove.add(kafkaSession.getEdge().getId());
}
}
for (EdgeId edgeId : toRemove) {
log.info("[{}] Destroying session for edge because edge is not connected", edgeId);
EdgeGrpcSession removed = sessions.remove(edgeId);
if (removed instanceof KafkaEdgeGrpcSession kafkaSession) {
kafkaSession.destroy();
}
}
} catch (Exception e) {
log.warn("Failed to cleanup kafka sessions", e);
}
}
} }

58
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -292,11 +292,11 @@ public abstract class EdgeGrpcSession implements Closeable {
protected void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture<Pair<Long, Long>> result) { protected void processEdgeEvents(EdgeEventFetcher fetcher, PageLink pageLink, SettableFuture<Pair<Long, Long>> result) {
try { try {
log.trace("[{}] Start processing edge events, fetcher = {}, pageLink = {}", sessionId, fetcher.getClass().getSimpleName(), pageLink); log.trace("[{}] Start processing edge events, fetcher = {}, pageLink = {}", edge.getId(), fetcher.getClass().getSimpleName(), pageLink);
processHighPriorityEvents(); processHighPriorityEvents();
PageData<EdgeEvent> pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink); PageData<EdgeEvent> pageData = fetcher.fetchEdgeEvents(edge.getTenantId(), edge, pageLink);
if (isConnected() && !pageData.getData().isEmpty()) { if (isConnected() && !pageData.getData().isEmpty()) {
log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, sessionId, pageData.getData().size()); log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, edge.getId(), pageData.getData().size());
List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData());
Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() { Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() {
@Override @Override
@ -323,16 +323,16 @@ public abstract class EdgeGrpcSession implements Closeable {
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
log.error("[{}] Failed to send downlink msgs pack", sessionId, t); log.error("[{}] Failed to send downlink msgs pack", edge.getId(), t);
result.setException(t); result.setException(t);
} }
}, ctx.getGrpcCallbackExecutorService()); }, ctx.getGrpcCallbackExecutorService());
} else { } else {
log.trace("[{}] no event(s) found. Stop processing edge events, fetcher = {}, pageLink = {}", sessionId, fetcher.getClass().getSimpleName(), pageLink); log.trace("[{}] no event(s) found. Stop processing edge events, fetcher = {}, pageLink = {}", edge.getId(), fetcher.getClass().getSimpleName(), pageLink);
result.set(null); result.set(null);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("[{}] Failed to fetch edge events", sessionId, e); log.error("[{}] Failed to fetch edge events", edge.getId(), e);
result.setException(e); result.setException(e);
} }
} }
@ -459,9 +459,9 @@ public abstract class EdgeGrpcSession implements Closeable {
ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId) ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId)
.edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build()); .edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build());
} }
log.warn("[{}][{}] {}, attempt: {}", tenantId, sessionId, failureMsg, attempt); log.warn("[{}][{}] {}, attempt: {}", tenantId, edge.getId(), failureMsg, attempt);
} }
log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", tenantId, sessionId, copy.size()); log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", tenantId, edge.getId(), copy.size());
for (DownlinkMsg downlinkMsg : copy) { for (DownlinkMsg downlinkMsg : copy) {
if (clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > clientMaxInboundMessageSize) { if (clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > clientMaxInboundMessageSize) {
String error = String.format("Client max inbound message size %s is exceeded. Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE " + String error = String.format("Client max inbound message size %s is exceeded. Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE " +
@ -483,7 +483,7 @@ public abstract class EdgeGrpcSession implements Closeable {
} else { } else {
String failureMsg = String.format("Failed to deliver messages: %s", copy); String failureMsg = String.format("Failed to deliver messages: %s", copy);
log.warn("[{}][{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", log.warn("[{}][{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}",
tenantId, sessionId, MAX_DOWNLINK_ATTEMPTS, copy); tenantId, edge.getId(), MAX_DOWNLINK_ATTEMPTS, copy);
ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg) .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg)
.error("Failed to deliver messages after " + MAX_DOWNLINK_ATTEMPTS + " attempts").build()); .error("Failed to deliver messages after " + MAX_DOWNLINK_ATTEMPTS + " attempts").build());
@ -493,7 +493,7 @@ public abstract class EdgeGrpcSession implements Closeable {
stopCurrentSendDownlinkMsgsTask(false); stopCurrentSendDownlinkMsgsTask(false);
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}][{}] Failed to send downlink msgs. Error msg {}", tenantId, sessionId, e.getMessage(), e); log.warn("[{}][{}] Failed to send downlink msgs. Error msg {}", tenantId, edge.getId(), e.getMessage(), e);
stopCurrentSendDownlinkMsgsTask(true); stopCurrentSendDownlinkMsgsTask(true);
} }
}; };
@ -540,7 +540,7 @@ public abstract class EdgeGrpcSession implements Closeable {
stopCurrentSendDownlinkMsgsTask(false); stopCurrentSendDownlinkMsgsTask(false);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("[{}][{}] Can't process downlink response message [{}]", tenantId, sessionId, msg, e); log.error("[{}][{}] Can't process downlink response message [{}]", tenantId, edge.getId(), msg, e);
} }
} }
@ -555,12 +555,12 @@ public abstract class EdgeGrpcSession implements Closeable {
while ((event = highPriorityQueue.poll()) != null) { while ((event = highPriorityQueue.poll()) != null) {
highPriorityEvents.add(event); highPriorityEvents.add(event);
} }
log.trace("[{}][{}] Sending high priority events {}", tenantId, sessionId, highPriorityEvents.size()); log.trace("[{}][{}] Sending high priority events {}", tenantId, edge.getId(), highPriorityEvents.size());
List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(highPriorityEvents); List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(highPriorityEvents);
sendDownlinkMsgsPack(downlinkMsgsPack).get(); sendDownlinkMsgsPack(downlinkMsgsPack).get();
} }
} catch (Exception e) { } catch (Exception e) {
log.error("[{}] Failed to process high priority events", sessionId, e); log.error("[{}] Failed to process high priority events", edge.getId(), e);
} }
} }
@ -577,7 +577,7 @@ public abstract class EdgeGrpcSession implements Closeable {
Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()), Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()),
ctx.getEdgeEventService()); ctx.getEdgeEventService());
log.trace("[{}][{}] starting processing edge events, previousStartTs = {}, previousStartSeqId = {}", log.trace("[{}][{}] starting processing edge events, previousStartTs = {}, previousStartSeqId = {}",
tenantId, sessionId, previousStartTs, previousStartSeqId); tenantId, edge.getId(), previousStartTs, previousStartSeqId);
Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() { Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable Pair<Long, Long> newStartTsAndSeqId) { public void onSuccess(@Nullable Pair<Long, Long> newStartTsAndSeqId) {
@ -586,7 +586,7 @@ public abstract class EdgeGrpcSession implements Closeable {
Futures.addCallback(updateFuture, new FutureCallback<>() { Futures.addCallback(updateFuture, new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable AttributesSaveResult saveResult) { public void onSuccess(@Nullable AttributesSaveResult saveResult) {
log.debug("[{}][{}] queue offset was updated [{}]", tenantId, sessionId, newStartTsAndSeqId); log.debug("[{}][{}] queue offset was updated [{}]", tenantId, edge.getId(), newStartTsAndSeqId);
boolean newEventsAvailable; boolean newEventsAvailable;
if (fetcher.isSeqIdNewCycleStarted()) { if (fetcher.isSeqIdNewCycleStarted()) {
newEventsAvailable = isNewEdgeEventsAvailable(); newEventsAvailable = isNewEdgeEventsAvailable();
@ -601,28 +601,28 @@ public abstract class EdgeGrpcSession implements Closeable {
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
log.error("[{}][{}] Failed to update queue offset [{}]", tenantId, sessionId, newStartTsAndSeqId, t); log.error("[{}][{}] Failed to update queue offset [{}]", tenantId, edge.getId(), newStartTsAndSeqId, t);
result.setException(t); result.setException(t);
} }
}, ctx.getGrpcCallbackExecutorService()); }, ctx.getGrpcCallbackExecutorService());
} else { } else {
log.trace("[{}][{}] newStartTsAndSeqId is null. Skipping iteration without db update", tenantId, sessionId); log.trace("[{}][{}] newStartTsAndSeqId is null. Skipping iteration without db update", tenantId, edge.getId());
result.set(Boolean.FALSE); result.set(Boolean.FALSE);
} }
} }
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
log.error("[{}][{}] Failed to process events", tenantId, sessionId, t); log.error("[{}][{}] Failed to process events", tenantId, edge.getId(), t);
result.setException(t); result.setException(t);
} }
}, ctx.getGrpcCallbackExecutorService()); }, ctx.getGrpcCallbackExecutorService());
} else { } else {
if (isSyncInProgress()) { if (isSyncInProgress()) {
log.trace("[{}][{}] edge sync is not completed yet. Skipping iteration", tenantId, sessionId); log.trace("[{}][{}] edge sync is not completed yet. Skipping iteration", tenantId, edge.getId());
result.set(Boolean.TRUE); result.set(Boolean.TRUE);
} else { } else {
log.trace("[{}][{}] edge is not connected. Skipping iteration", tenantId, sessionId); log.trace("[{}][{}] edge is not connected. Skipping iteration", tenantId, edge.getId());
result.set(null); result.set(null);
} }
} }
@ -632,7 +632,7 @@ public abstract class EdgeGrpcSession implements Closeable {
protected List<DownlinkMsg> convertToDownlinkMsgsPack(List<EdgeEvent> edgeEvents) { protected List<DownlinkMsg> convertToDownlinkMsgsPack(List<EdgeEvent> edgeEvents) {
List<DownlinkMsg> result = new ArrayList<>(); List<DownlinkMsg> result = new ArrayList<>();
for (EdgeEvent edgeEvent : edgeEvents) { for (EdgeEvent edgeEvent : edgeEvents) {
log.trace("[{}][{}] converting edge event to downlink msg [{}]", tenantId, sessionId, edgeEvent); log.trace("[{}][{}] converting edge event to downlink msg [{}]", tenantId, edge.getId(), edgeEvent);
DownlinkMsg downlinkMsg = null; DownlinkMsg downlinkMsg = null;
try { try {
switch (edgeEvent.getAction()) { switch (edgeEvent.getAction()) {
@ -641,16 +641,16 @@ public abstract class EdgeGrpcSession implements Closeable {
ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> { ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> {
downlinkMsg = convertEntityEventToDownlink(edgeEvent); downlinkMsg = convertEntityEventToDownlink(edgeEvent);
if (downlinkMsg != null && downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) { if (downlinkMsg != null && downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) {
log.trace("[{}][{}] widgetTypeUpdateMsg message processed, downlinkMsgId = {}", tenantId, sessionId, downlinkMsg.getDownlinkMsgId()); log.trace("[{}][{}] widgetTypeUpdateMsg message processed, downlinkMsgId = {}", tenantId, edge.getId(), downlinkMsg.getDownlinkMsgId());
} else { } else {
log.trace("[{}][{}] entity message processed [{}]", tenantId, sessionId, downlinkMsg); log.trace("[{}][{}] entity message processed [{}]", tenantId, edge.getId(), downlinkMsg);
} }
} }
case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent); case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent);
default -> log.warn("[{}][{}] Unsupported action type [{}]", tenantId, sessionId, edgeEvent.getAction()); default -> log.warn("[{}][{}] Unsupported action type [{}]", tenantId, edge.getId(), edgeEvent.getAction());
} }
} catch (Exception e) { } catch (Exception e) {
log.trace("[{}][{}] Exception during converting edge event to downlink msg", tenantId, sessionId, e); log.trace("[{}][{}] Exception during converting edge event to downlink msg", tenantId, edge.getId(), e);
} }
if (downlinkMsg != null) { if (downlinkMsg != null) {
result.add(downlinkMsg); result.add(downlinkMsg);
@ -757,19 +757,19 @@ public abstract class EdgeGrpcSession implements Closeable {
private void sendDownlinkMsg(ResponseMsg responseMsg) { private void sendDownlinkMsg(ResponseMsg responseMsg) {
if (isConnected()) { if (isConnected()) {
String responseMsgStr = StringUtils.truncate(responseMsg.toString(), 10000); String responseMsgStr = StringUtils.truncate(responseMsg.toString(), 10000);
log.trace("[{}][{}] Sending downlink msg [{}]", tenantId, sessionId, responseMsgStr); log.trace("[{}][{}] Sending downlink msg [{}]", tenantId, edge.getId(), responseMsgStr);
downlinkMsgLock.lock(); downlinkMsgLock.lock();
String downlinkMsgStr = responseMsg.hasDownlinkMsg() ? String.valueOf(responseMsg.getDownlinkMsg().getDownlinkMsgId()) : responseMsgStr; String downlinkMsgStr = responseMsg.hasDownlinkMsg() ? String.valueOf(responseMsg.getDownlinkMsg().getDownlinkMsgId()) : responseMsgStr;
try { try {
outputStream.onNext(responseMsg); outputStream.onNext(responseMsg);
} catch (Exception e) { } catch (Exception e) {
log.trace("[{}][{}] Failed to send downlink message [{}]", tenantId, sessionId, downlinkMsgStr, e); log.trace("[{}][{}] Failed to send downlink message [{}]", tenantId, edge.getId(), downlinkMsgStr, e);
connected = false; connected = false;
sessionCloseListener.accept(edge, sessionId); sessionCloseListener.accept(edge, sessionId);
} finally { } finally {
downlinkMsgLock.unlock(); downlinkMsgLock.unlock();
} }
log.trace("[{}][{}] downlink msg successfully sent [{}]", tenantId, sessionId, downlinkMsgStr); log.trace("[{}][{}] downlink msg successfully sent [{}]", tenantId, edge.getId(), downlinkMsgStr);
} }
} }
@ -909,8 +909,8 @@ public abstract class EdgeGrpcSession implements Closeable {
} }
} catch (Exception e) { } catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.trace("[{}][{}] Can't process uplink msg [{}]", edge.getTenantId(), sessionId, uplinkMsg, e); log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e);
ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(edge.getTenantId()).edgeId(edge.getId()) ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build()); .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build());
return Futures.immediateFailedFuture(e); return Futures.immediateFailedFuture(e);
} }

90
application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.edge.rpc;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import io.grpc.stub.StreamObserver; import io.grpc.stub.StreamObserver;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
@ -56,6 +57,7 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
private volatile boolean isHighPriorityProcessing; private volatile boolean isHighPriorityProcessing;
@Getter
private QueueConsumerManager<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumer; private QueueConsumerManager<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumer;
private ExecutorService consumerExecutor; private ExecutorService consumerExecutor;
@ -72,31 +74,28 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
} }
private void processMsgs(List<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumer) { private void processMsgs(List<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> msgs, TbQueueConsumer<TbProtoQueueMsg<ToEdgeEventNotificationMsg>> consumer) {
log.trace("[{}][{}] starting processing edge events", tenantId, sessionId); log.trace("[{}][{}] starting processing edge events", tenantId, edge.getId());
if (isConnected() && !isSyncInProgress() && !isHighPriorityProcessing) { if (!isConnected() || isSyncInProgress() || isHighPriorityProcessing) {
List<EdgeEvent> edgeEvents = new ArrayList<>(); log.debug("[{}][{}] edge not connected, edge sync is not completed or high priority processing in progress, " +
for (TbProtoQueueMsg<ToEdgeEventNotificationMsg> msg : msgs) { "connected = {}, sync in progress = {}, high priority in progress = {}. Skipping iteration",
EdgeEvent edgeEvent = ProtoUtils.fromProto(msg.getValue().getEdgeEventMsg()); tenantId, edge.getId(), isConnected(), isSyncInProgress(), isHighPriorityProcessing);
edgeEvents.add(edgeEvent); return;
} }
List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(edgeEvents); List<EdgeEvent> edgeEvents = new ArrayList<>();
try { for (TbProtoQueueMsg<ToEdgeEventNotificationMsg> msg : msgs) {
boolean isInterrupted = sendDownlinkMsgsPack(downlinkMsgsPack).get(); EdgeEvent edgeEvent = ProtoUtils.fromProto(msg.getValue().getEdgeEventMsg());
if (isInterrupted) { edgeEvents.add(edgeEvent);
log.debug("[{}][{}][{}] Send downlink messages task was interrupted", tenantId, edge.getId(), sessionId); }
} else { List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(edgeEvents);
consumer.commit(); try {
} boolean isInterrupted = sendDownlinkMsgsPack(downlinkMsgsPack).get();
} catch (Exception e) { if (isInterrupted) {
log.error("[{}] Failed to process all downlink messages", sessionId, e); log.debug("[{}][{}] Send downlink messages task was interrupted", tenantId, edge.getId());
} } else {
} else { consumer.commit();
try {
Thread.sleep(ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval());
} catch (InterruptedException interruptedException) {
log.trace("Failed to wait until the server has capacity to handle new requests", interruptedException);
} }
log.trace("[{}][{}] edge is not connected or sync is not completed. Skipping iteration", tenantId, sessionId); } catch (Exception e) {
log.error("[{}][{}] Failed to process downlink messages", tenantId, edge.getId(), e);
} }
} }
@ -107,18 +106,23 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
@Override @Override
public ListenableFuture<Boolean> processEdgeEvents() { public ListenableFuture<Boolean> processEdgeEvents() {
if (consumer == null) { if (consumer == null || (consumer.getConsumer() != null && consumer.getConsumer().isStopped())) {
this.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer")); try {
this.consumer = QueueConsumerManager.<TbProtoQueueMsg<ToEdgeEventNotificationMsg>>builder() this.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer"));
.name("TB Edge events") this.consumer = QueueConsumerManager.<TbProtoQueueMsg<ToEdgeEventNotificationMsg>>builder()
.msgPackProcessor(this::processMsgs) .name("TB Edge events [" + edge.getId() + "]")
.pollInterval(ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval()) .msgPackProcessor(this::processMsgs)
.consumerCreator(() -> tbCoreQueueFactory.createEdgeEventMsgConsumer(tenantId, edge.getId())) .pollInterval(ctx.getEdgeEventStorageSettings().getNoRecordsSleepInterval())
.consumerExecutor(consumerExecutor) .consumerCreator(() -> tbCoreQueueFactory.createEdgeEventMsgConsumer(tenantId, edge.getId()))
.threadPrefix("edge-events") .consumerExecutor(consumerExecutor)
.build(); .threadPrefix("edge-events-" + edge.getId())
consumer.subscribe(); .build();
consumer.launch(); consumer.subscribe();
consumer.launch();
} catch (Exception e) {
destroy();
log.warn("[{}][{}] Failed to start edge event consumer", sessionId, edge.getId(), e);
}
} }
return Futures.immediateFuture(Boolean.FALSE); return Futures.immediateFuture(Boolean.FALSE);
} }
@ -132,8 +136,18 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
@Override @Override
public void destroy() { public void destroy() {
consumer.stop(); try {
consumerExecutor.shutdown(); if (consumer != null) {
consumer.stop();
}
} finally {
consumer = null;
}
try {
if (consumerExecutor != null) {
consumerExecutor.shutdown();
}
} catch (Exception ignored) {}
} }
@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) {

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

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';
@ -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