Browse Source

Merge remote-tracking branch 'origin/develop/3.5' into feature/sparkplug-shared-attributes

pull/8457/head
Andrii Shvaika 3 years ago
parent
commit
2278d98824
  1. 71
      application/src/main/data/json/system/widget_bundles/home_page_widgets.json
  2. 13
      application/src/main/data/upgrade/3.4.4/schema_update.sql
  3. 5
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  4. 114
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  5. 137
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  6. 15
      application/src/main/java/org/thingsboard/server/controller/NotificationController.java
  7. 25
      application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
  8. 1
      application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
  9. 5
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  10. 1
      application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java
  11. 6
      application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java
  12. 5
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java
  13. 11
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java
  14. 8
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java
  15. 6
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ApiUsageLimitTriggerProcessor.java
  16. 73
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java
  17. 6
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java
  18. 23
      application/src/main/java/org/thingsboard/server/service/slack/DefaultSlackService.java
  19. 4
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  20. 11
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
  21. 31
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  22. 8
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  23. 37
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  24. 25
      application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java
  25. 13
      application/src/test/java/org/thingsboard/server/service/stats/DevicesStatisticsTest.java
  26. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  27. 6
      common/data/src/main/java/org/thingsboard/server/common/data/User.java
  28. 39
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQueryV2.java
  29. 31
      common/data/src/main/java/org/thingsboard/server/common/data/exception/TenantNotFoundException.java
  30. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
  31. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java
  32. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java
  33. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntityActionNotificationInfo.java
  34. 24
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackConversation.java
  35. 29
      common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentClusteringMode.java
  36. 10
      common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
  37. 5
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
  38. 8
      common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java
  39. 47
      common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/EntityActionTrigger.java
  40. 3
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  41. 3
      common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java
  42. 5
      dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
  43. 11
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  44. 2
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  45. 7
      dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java
  46. 5
      dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java
  47. 8
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java
  48. 8
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
  49. 29
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  50. 4
      dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java
  51. 133
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java
  52. 80
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
  53. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java
  54. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/component/SqlComponentDescriptorInsertRepository.java
  55. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRuleRepository.java
  56. 4
      dao/src/main/resources/sql/schema-entities.sql
  57. 110
      dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java
  58. 2
      pull_request_template.md
  59. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java
  60. 5
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java
  61. 3
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java
  62. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/slack/SlackService.java
  63. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  64. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java
  65. 6
      ui-ngx/angular.json
  66. 32
      ui-ngx/src/app/core/api/alias-controller.ts
  67. 91
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  68. 4
      ui-ngx/src/app/core/api/entity-data.service.ts
  69. 14
      ui-ngx/src/app/core/http/alarm.service.ts
  70. 26
      ui-ngx/src/app/core/http/entity.service.ts
  71. 5
      ui-ngx/src/app/core/http/notification.service.ts
  72. 1
      ui-ngx/src/app/core/http/public-api.ts
  73. 37
      ui-ngx/src/app/core/http/usage-info.service.ts
  74. 23
      ui-ngx/src/app/core/http/user-settings.service.ts
  75. 69
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  76. 8
      ui-ngx/src/app/core/services/item-buffer.service.ts
  77. 1
      ui-ngx/src/app/core/services/menu.models.ts
  78. 193
      ui-ngx/src/app/core/services/menu.service.ts
  79. 15
      ui-ngx/src/app/core/ws/telemetry-websocket.service.ts
  80. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html
  81. 2
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts
  82. 203
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts
  83. 31
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html
  84. 193
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts
  85. 103
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html
  86. 29
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss
  87. 318
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts
  88. 22
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts
  89. 14
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html
  90. 6
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss
  91. 10
      ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts
  92. 61
      ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts
  93. 7
      ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts
  94. 2
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
  95. 3
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss
  96. 44
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  97. 8
      ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html
  98. 9
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  99. 1
      ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts
  100. 3
      ui-ngx/src/app/modules/home/components/notification/notification-bell.component.ts

71
application/src/main/data/json/system/widget_bundles/home_page_widgets.json

File diff suppressed because one or more lines are too long

13
application/src/main/data/upgrade/3.4.4/schema_update.sql

@ -614,3 +614,16 @@ END
$$; $$;
-- TTL DROP PARTITIONS FUNCTIONS UPDATE END -- TTL DROP PARTITIONS FUNCTIONS UPDATE END
-- RULE NODE SINGLETON MODE SUPPORT
ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS singleton_mode bool DEFAULT false;
UPDATE rule_node SET singleton_mode = true WHERE type IN ('org.thingsboard.rule.engine.mqtt.azure.TbAzureIotHubNode', 'org.thingsboard.rule.engine.mqtt.TbMqttNode');
ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS clustering_mode varchar(255) DEFAULT 'ENABLED';
UPDATE component_descriptor SET clustering_mode = 'USER_PREFERENCE' WHERE clazz = 'org.thingsboard.rule.engine.mqtt.TbMqttNode';
UPDATE component_descriptor SET clustering_mode = 'SINGLETON' WHERE clazz = 'org.thingsboard.rule.engine.mqtt.azure.TbAzureIotHubNode';

5
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -87,6 +87,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.queue.discovery.DiscoveryService;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
@ -179,6 +180,10 @@ public class ActorSystemContext {
@Setter @Setter
private ComponentDiscoveryService componentService; private ComponentDiscoveryService componentService;
@Autowired
@Getter
private DiscoveryService discoveryService;
@Autowired @Autowired
@Getter @Getter
private DataDecodingEncodingService encodingService; private DataDecodingEncodingService encodingService;

114
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java

@ -31,7 +31,10 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.RuleNodeException; import org.thingsboard.server.common.msg.queue.RuleNodeException;
import org.thingsboard.server.common.msg.queue.RuleNodeInfo; import org.thingsboard.server.common.msg.queue.RuleNodeInfo;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.gen.transport.TransportProtos;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
@ -39,11 +42,10 @@ import org.thingsboard.server.common.stats.TbApiUsageReportClient;
public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> { public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> {
private final String ruleChainName; private final String ruleChainName;
private final TbActorRef self;
private final TbApiUsageReportClient apiUsageClient; private final TbApiUsageReportClient apiUsageClient;
private final DefaultTbContext defaultCtx;
private RuleNode ruleNode; private RuleNode ruleNode;
private TbNode tbNode; private TbNode tbNode;
private DefaultTbContext defaultCtx;
private RuleNodeInfo info; private RuleNodeInfo info;
RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName, RuleNodeId ruleNodeId, ActorSystemContext systemContext RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName, RuleNodeId ruleNodeId, ActorSystemContext systemContext
@ -51,7 +53,6 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
super(systemContext, tenantId, ruleNodeId); super(systemContext, tenantId, ruleNodeId);
this.apiUsageClient = systemContext.getApiUsageClient(); this.apiUsageClient = systemContext.getApiUsageClient();
this.ruleChainName = ruleChainName; this.ruleChainName = ruleChainName;
this.self = self;
this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId); this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
this.defaultCtx = new DefaultTbContext(systemContext, ruleChainName, new RuleNodeCtx(tenantId, parent, self, ruleNode)); this.defaultCtx = new DefaultTbContext(systemContext, ruleChainName, new RuleNodeCtx(tenantId, parent, self, ruleNode));
this.info = new RuleNodeInfo(ruleNodeId, ruleChainName, ruleNode != null ? ruleNode.getName() : "Unknown"); this.info = new RuleNodeInfo(ruleNodeId, ruleChainName, ruleNode != null ? ruleNode.getName() : "Unknown");
@ -59,29 +60,36 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
@Override @Override
public void start(TbActorCtx context) throws Exception { public void start(TbActorCtx context) throws Exception {
tbNode = initComponent(ruleNode); if (isMyNodePartition()) {
if (tbNode != null) { tbNode = initComponent(ruleNode);
state = ComponentLifecycleState.ACTIVE; if (tbNode != null) {
state = ComponentLifecycleState.ACTIVE;
}
} }
} }
@Override @Override
public void onUpdate(TbActorCtx context) throws Exception { public void onUpdate(TbActorCtx context) throws Exception {
RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId); if (isMyNodePartition()) {
this.info = new RuleNodeInfo(entityId, ruleChainName, newRuleNode != null ? newRuleNode.getName() : "Unknown"); RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
boolean restartRequired = state != ComponentLifecycleState.ACTIVE || this.info = new RuleNodeInfo(entityId, ruleChainName, newRuleNode != null ? newRuleNode.getName() : "Unknown");
!(ruleNode.getType().equals(newRuleNode.getType()) && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration())); boolean restartRequired = state != ComponentLifecycleState.ACTIVE ||
this.ruleNode = newRuleNode; !(ruleNode.getType().equals(newRuleNode.getType()) && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
this.defaultCtx.updateSelf(newRuleNode); this.ruleNode = newRuleNode;
if (restartRequired) { this.defaultCtx.updateSelf(newRuleNode);
if (tbNode != null) { if (restartRequired) {
tbNode.destroy(); if (tbNode != null) {
} tbNode.destroy();
try { }
start(context); try {
} catch (Exception e) { start(context);
throw new TbRuleNodeUpdateException("Failed to update rule node", e); } catch (Exception e) {
throw new TbRuleNodeUpdateException("Failed to update rule node", e);
}
} }
} else if (tbNode != null) {
stop(null);
tbNode = null;
} }
} }
@ -94,9 +102,16 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
} }
@Override @Override
public void onPartitionChangeMsg(PartitionChangeMsg msg) { public void onPartitionChangeMsg(PartitionChangeMsg msg) throws Exception {
if (tbNode != null) { if (tbNode != null) {
tbNode.onPartitionChangeMsg(defaultCtx, msg); if (!isMyNodePartition()) {
stop(null);
tbNode = null;
} else {
tbNode.onPartitionChangeMsg(defaultCtx, msg);
}
} else if (isMyNodePartition()) {
start(null);
} }
} }
@ -121,23 +136,27 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
} }
void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception { void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception {
msg.getMsg().getCallback().onProcessingStart(info); if (!isMyNodePartition()) {
checkComponentStateActive(msg.getMsg()); putToNodePartition(msg.getMsg());
TbMsg tbMsg = msg.getMsg();
int ruleNodeCount = tbMsg.getAndIncrementRuleNodeCounter();
int maxRuleNodeExecutionsPerMessage = getTenantProfileConfiguration().getMaxRuleNodeExecsPerMessage();
if (maxRuleNodeExecutionsPerMessage == 0 || ruleNodeCount < maxRuleNodeExecutionsPerMessage) {
apiUsageClient.report(tenantId, tbMsg.getCustomerId(), ApiUsageRecordKey.RE_EXEC_COUNT);
if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), msg.getFromRelationType());
}
try {
tbNode.onMsg(msg.getCtx(), msg.getMsg());
} catch (Exception e) {
msg.getCtx().tellFailure(msg.getMsg(), e);
}
} else { } else {
tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more then " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode)); msg.getMsg().getCallback().onProcessingStart(info);
checkComponentStateActive(msg.getMsg());
TbMsg tbMsg = msg.getMsg();
int ruleNodeCount = tbMsg.getAndIncrementRuleNodeCounter();
int maxRuleNodeExecutionsPerMessage = getTenantProfileConfiguration().getMaxRuleNodeExecsPerMessage();
if (maxRuleNodeExecutionsPerMessage == 0 || ruleNodeCount < maxRuleNodeExecutionsPerMessage) {
apiUsageClient.report(tenantId, tbMsg.getCustomerId(), ApiUsageRecordKey.RE_EXEC_COUNT);
if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), msg.getFromRelationType());
}
try {
tbNode.onMsg(msg.getCtx(), msg.getMsg());
} catch (Exception e) {
msg.getCtx().tellFailure(msg.getMsg(), e);
}
} else {
tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more then " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode));
}
} }
} }
@ -160,4 +179,23 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
protected RuleNodeException getInactiveException() { protected RuleNodeException getInactiveException() {
return new RuleNodeException("Rule Node is not active! Failed to initialize.", ruleChainName, ruleNode); return new RuleNodeException("Rule Node is not active! Failed to initialize.", ruleChainName, ruleNode);
} }
private boolean isMyNodePartition() {
return !ruleNode.isSingletonMode()
|| systemContext.getDiscoveryService().isMonolith()
|| defaultCtx.isLocalEntity(ruleNode.getId());
}
//Message will return after processing. See RuleChainActorMessageProcessor.pushToTarget.
private void putToNodePartition(TbMsg source) {
TbMsg tbMsg = TbMsg.newMsg(source, source.getQueueName(), source.getRuleChainId(), entityId);
TopicPartitionInfo tpi = systemContext.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, ruleNode.getId());
TransportProtos.ToRuleEngineMsg toQueueMsg = TransportProtos.ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(tbMsg))
.build();
systemContext.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), toQueueMsg, null);
defaultCtx.ack(source);
}
} }

137
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
@ -52,6 +53,10 @@ import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -85,9 +90,15 @@ public class AlarmController extends BaseController {
private static final String ALARM_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the originator of alarm is owned by the same tenant. " + private static final String ALARM_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the originator of alarm is owned by the same tenant. " +
"If the user has the authority of 'Customer User', the server checks that the originator of alarm belongs to the customer. "; "If the user has the authority of 'Customer User', the server checks that the originator of alarm belongs to the customer. ";
private static final String ALARM_QUERY_SEARCH_STATUS_DESCRIPTION = "A string value representing one of the AlarmSearchStatus enumeration value"; private static final String ALARM_QUERY_SEARCH_STATUS_DESCRIPTION = "A string value representing one of the AlarmSearchStatus enumeration value";
private static final String ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing one of the AlarmSearchStatus enumeration value";
private static final String ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES = "ANY, ACTIVE, CLEARED, ACK, UNACK"; private static final String ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES = "ANY, ACTIVE, CLEARED, ACK, UNACK";
private static final String ALARM_QUERY_STATUS_DESCRIPTION = "A string value representing one of the AlarmStatus enumeration value"; private static final String ALARM_QUERY_STATUS_DESCRIPTION = "A string value representing one of the AlarmStatus enumeration value";
private static final String ALARM_QUERY_STATUS_ALLOWABLE_VALUES = "ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK"; private static final String ALARM_QUERY_STATUS_ALLOWABLE_VALUES = "ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK";
private static final String ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing one of the AlarmSeverity enumeration value";
private static final String ALARM_QUERY_SEVERITY_ALLOWABLE_VALUES = "CRITICAL, MAJOR, MINOR, WARNING, INDETERMINATE";
private static final String ALARM_QUERY_TYPE_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing alarm types";
private static final String ALARM_QUERY_ASSIGNEE_DESCRIPTION = "A string value representing the assignee user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; private static final String ALARM_QUERY_ASSIGNEE_DESCRIPTION = "A string value representing the assignee user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
private static final String ALARM_QUERY_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on of next alarm fields: type, severity or status"; private static final String ALARM_QUERY_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on of next alarm fields: type, severity or status";
private static final String ALARM_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'."; private static final String ALARM_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'.";
@ -331,6 +342,132 @@ public class AlarmController extends BaseController {
} }
} }
@ApiOperation(value = "Get Alarms (getAlarmsV2)",
notes = "Returns a page of alarms for the selected entity. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/v2/alarm/{entityType}/{entityId}", method = RequestMethod.GET)
@ResponseBody
public PageData<AlarmInfo> getAlarmsV2(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE")
@PathVariable(ENTITY_TYPE) String strEntityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(ENTITY_ID) String strEntityId,
@ApiParam(value = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String[] statusList,
@ApiParam(value = ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEVERITY_ALLOWABLE_VALUES)
@RequestParam(required = false) String[] severityList,
@ApiParam(value = ALARM_QUERY_TYPE_ARRAY_DESCRIPTION)
@RequestParam(required = false) String[] typeList,
@ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION)
@RequestParam(required = false) String assigneeId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder,
@ApiParam(value = ALARM_QUERY_START_TIME_DESCRIPTION)
@RequestParam(required = false) Long startTime,
@ApiParam(value = ALARM_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime
) throws ThingsboardException, ExecutionException, InterruptedException {
checkParameter("EntityId", strEntityId);
checkParameter("EntityType", strEntityType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId);
checkEntityId(entityId, Operation.READ);
List<AlarmSearchStatus> alarmStatusList = new ArrayList<>();
if (statusList != null) {
for (String strStatus : statusList) {
if (!StringUtils.isEmpty(strStatus)) {
alarmStatusList.add(AlarmSearchStatus.valueOf(strStatus));
}
}
}
List<AlarmSeverity> alarmSeverityList = new ArrayList<>();
if (severityList != null) {
for (String strSeverity : severityList) {
if (!StringUtils.isEmpty(strSeverity)) {
alarmSeverityList.add(AlarmSeverity.valueOf(strSeverity));
}
}
}
List<String> alarmTypeList = typeList != null ? Arrays.asList(typeList) : Collections.emptyList();
UserId assigneeUserId = null;
if (assigneeId != null) {
assigneeUserId = new UserId(UUID.fromString(assigneeId));
}
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(entityId, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get());
}
@ApiOperation(value = "Get All Alarms (getAllAlarmsV2)",
notes = "Returns a page of alarms that belongs to the current user owner. " +
"If the user has the authority of 'Tenant Administrator', the server returns alarms that belongs to the tenant of current user. " +
"If the user has the authority of 'Customer User', the server returns alarms that belongs to the customer of current user. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/v2/alarms", method = RequestMethod.GET)
@ResponseBody
public PageData<AlarmInfo> getAllAlarmsV2(
@ApiParam(value = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String[] statusList,
@ApiParam(value = ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEVERITY_ALLOWABLE_VALUES)
@RequestParam(required = false) String[] severityList,
@ApiParam(value = ALARM_QUERY_TYPE_ARRAY_DESCRIPTION)
@RequestParam(required = false) String[] typeList,
@ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION)
@RequestParam(required = false) String assigneeId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder,
@ApiParam(value = ALARM_QUERY_START_TIME_DESCRIPTION)
@RequestParam(required = false) Long startTime,
@ApiParam(value = ALARM_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime
) throws ThingsboardException, ExecutionException, InterruptedException {
List<AlarmSearchStatus> alarmStatusList = new ArrayList<>();
if (statusList != null) {
for (String strStatus : statusList) {
if (!StringUtils.isEmpty(strStatus)) {
alarmStatusList.add(AlarmSearchStatus.valueOf(strStatus));
}
}
}
List<AlarmSeverity> alarmSeverityList = new ArrayList<>();
if (severityList != null) {
for (String strSeverity : severityList) {
if (!StringUtils.isEmpty(strSeverity)) {
alarmSeverityList.add(AlarmSeverity.valueOf(strSeverity));
}
}
}
List<String> alarmTypeList = typeList != null ? Arrays.asList(typeList) : Collections.emptyList();
UserId assigneeUserId = null;
if (assigneeId != null) {
assigneeUserId = new UserId(UUID.fromString(assigneeId));
}
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
if (getCurrentUser().isCustomerUser()) {
return checkNotNull(alarmService.findCustomerAlarmsV2(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get());
} else {
return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get());
}
}
@ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)", @ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)",
notes = "Search the alarms by originator ('entityType' and entityId') and optional 'status' or 'searchStatus' filters and returns the highest AlarmSeverity(CRITICAL, MAJOR, MINOR, WARNING or INDETERMINATE). " + notes = "Search the alarms by originator ('entityType' and entityId') and optional 'status' or 'searchStatus' filters and returns the highest AlarmSeverity(CRITICAL, MAJOR, MINOR, WARNING or INDETERMINATE). " +
"Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH

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

@ -47,11 +47,13 @@ import org.thingsboard.server.common.data.notification.targets.NotificationRecip
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; import org.thingsboard.server.common.data.notification.targets.NotificationTargetType;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationService; import org.thingsboard.server.dao.notification.NotificationService;
import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.notification.NotificationSettingsService;
@ -230,7 +232,8 @@ public class NotificationController extends BaseController {
NotificationTargetType targetType = target.getConfiguration().getType(); NotificationTargetType targetType = target.getConfiguration().getType();
if (targetType == NotificationTargetType.PLATFORM_USERS) { if (targetType == NotificationTargetType.PLATFORM_USERS) {
PageData<User> recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), PageData<User> recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(),
(PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize)); (PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize, 0, null,
new SortOrder("createdTime", SortOrder.Direction.DESC)));
recipientsCount = (int) recipients.getTotalElements(); recipientsCount = (int) recipients.getTotalElements();
recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList()); recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList());
} else { } else {
@ -240,7 +243,15 @@ public class NotificationController extends BaseController {
firstRecipient.putIfAbsent(targetType, !recipientsPart.isEmpty() ? recipientsPart.get(0) : null); firstRecipient.putIfAbsent(targetType, !recipientsPart.isEmpty() ? recipientsPart.get(0) : null);
for (NotificationRecipient recipient : recipientsPart) { for (NotificationRecipient recipient : recipientsPart) {
if (recipientsPreview.size() < recipientsPreviewSize) { if (recipientsPreview.size() < recipientsPreviewSize) {
recipientsPreview.add(recipient.getTitle()); String title = recipient.getTitle();
if (recipient instanceof SlackConversation) {
title = ((SlackConversation) recipient).getPointer() + title;
} else if (recipient instanceof User) {
if (!title.equals(recipient.getEmail())) {
title += " (" + recipient.getEmail() + ")";
}
}
recipientsPreview.add(title);
} else { } else {
break; break;
} }

25
application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java

@ -42,6 +42,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger; import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger;
import org.thingsboard.server.common.msg.notification.trigger.EntityActionTrigger;
import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.queue.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
@ -190,12 +191,6 @@ public class EntityActionService {
AlarmComment comment = extractParameter(AlarmComment.class, 0, additionalInfo); AlarmComment comment = extractParameter(AlarmComment.class, 0, additionalInfo);
metaData.putValue("comment", json.writeValueAsString(comment)); metaData.putValue("comment", json.writeValueAsString(comment));
} }
if (actionType == ActionType.ADDED && !tenantId.isSysTenantId()) {
notificationRuleProcessor.process(EntitiesLimitTrigger.builder()
.tenantId(tenantId)
.entityType(entityId.getEntityType())
.build());
}
ObjectNode entityNode; ObjectNode entityNode;
if (entity != null) { if (entity != null) {
entityNode = json.valueToTree(entity); entityNode = json.valueToTree(entity);
@ -242,12 +237,28 @@ public class EntityActionService {
entityNode = json.valueToTree(extractParameter(EntityRelation.class, 0, additionalInfo)); entityNode = json.valueToTree(extractParameter(EntityRelation.class, 0, additionalInfo));
} }
} }
TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, json.writeValueAsString(entityNode));
if (tenantId == null || tenantId.isNullUid()) { if (tenantId == null || tenantId.isNullUid()) {
if (entity instanceof HasTenantId) { if (entity instanceof HasTenantId) {
tenantId = ((HasTenantId) entity).getTenantId(); tenantId = ((HasTenantId) entity).getTenantId();
} }
} }
if (tenantId != null && !tenantId.isSysTenantId()) {
if (actionType == ActionType.ADDED) {
notificationRuleProcessor.process(EntitiesLimitTrigger.builder()
.tenantId(tenantId)
.entityType(entityId.getEntityType())
.build());
}
notificationRuleProcessor.process(EntityActionTrigger.builder()
.tenantId(tenantId)
.entityId(entityId)
.entity(entity)
.actionType(actionType)
.user(user)
.build());
}
TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, json.writeValueAsString(entityNode));
tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, null); tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, null);
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}] Failed to push entity action to rule engine: {}", entityId, actionType, e); log.warn("[{}] Failed to push entity action to rule engine: {}", entityId, actionType, e);

1
application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java

@ -155,6 +155,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class); RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
scannedComponent.setName(ruleNodeAnnotation.name()); scannedComponent.setName(ruleNodeAnnotation.name());
scannedComponent.setScope(ruleNodeAnnotation.scope()); scannedComponent.setScope(ruleNodeAnnotation.scope());
scannedComponent.setClusteringMode(ruleNodeAnnotation.clusteringMode());
NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation); NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation);
ObjectNode configurationDescriptor = mapper.createObjectNode(); ObjectNode configurationDescriptor = mapper.createObjectNode();
JsonNode node = mapper.valueToTree(nodeDefinition); JsonNode node = mapper.valueToTree(nodeDefinition);

5
application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java

@ -364,6 +364,11 @@ public class DefaultMailService implements MailService {
mailSender.testConnection(); mailSender.testConnection();
} }
@Override
public boolean isConfigured(TenantId tenantId) {
return mailSender != null;
}
private String toEnabledValueLabel(ApiFeature apiFeature) { private String toEnabledValueLabel(ApiFeature apiFeature) {
switch (apiFeature) { switch (apiFeature) {
case DB: case DB:

1
application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java

@ -128,6 +128,7 @@ public class NotificationProcessingContext {
private Map<String, String> createTemplateContextForRecipient(NotificationRecipient recipient) { private Map<String, String> createTemplateContextForRecipient(NotificationRecipient recipient) {
return Map.of( return Map.of(
"recipientTitle", recipient.getTitle(),
"recipientEmail", Strings.nullToEmpty(recipient.getEmail()), "recipientEmail", Strings.nullToEmpty(recipient.getEmail()),
"recipientFirstName", Strings.nullToEmpty(recipient.getFirstName()), "recipientFirstName", Strings.nullToEmpty(recipient.getFirstName()),
"recipientLastName", Strings.nullToEmpty(recipient.getLastName()) "recipientLastName", Strings.nullToEmpty(recipient.getLastName())

6
application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java

@ -49,10 +49,8 @@ public class EmailNotificationChannel implements NotificationChannel<User, Email
@Override @Override
public void check(TenantId tenantId) throws Exception { public void check(TenantId tenantId) throws Exception {
try { if (!mailService.isConfigured(tenantId)) {
mailService.testConnection(tenantId); throw new RuntimeException("Mail server is not configured");
} catch (Exception e) {
throw new RuntimeException("Mail server is not available");
} }
} }

5
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java

@ -32,6 +32,7 @@ import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigg
import java.util.Set; import java.util.Set;
import static org.apache.commons.collections.CollectionUtils.isEmpty; import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service @Service
public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmAssignmentNotificationRuleTriggerConfig> { public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmAssignmentNotificationRuleTriggerConfig> {
@ -43,8 +44,8 @@ public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificatio
return false; return false;
} }
Alarm alarm = JacksonUtil.fromString(trigger.getMsg().getData(), Alarm.class); Alarm alarm = JacksonUtil.fromString(trigger.getMsg().getData(), Alarm.class);
return (isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType())) && return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()) &&
(isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity())) && emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()) &&
(isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm)); (isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm));
} }

11
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigg
import java.util.Set; import java.util.Set;
import static org.apache.commons.collections.CollectionUtils.isEmpty; import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service @Service
public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmCommentNotificationRuleTriggerConfig> { public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmCommentNotificationRuleTriggerConfig> {
@ -53,8 +54,8 @@ public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRu
} }
} }
Alarm alarm = JacksonUtil.fromString(msg.getData(), Alarm.class); Alarm alarm = JacksonUtil.fromString(msg.getData(), Alarm.class);
return (isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType())) && return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()) &&
(isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity())) && emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()) &&
(isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm)); (isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm));
} }
@ -66,9 +67,9 @@ public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRu
return AlarmCommentNotificationInfo.builder() return AlarmCommentNotificationInfo.builder()
.comment(comment.getComment().get("text").asText()) .comment(comment.getComment().get("text").asText())
.action(msg.getType().equals(DataConstants.COMMENT_CREATED) ? "added" : "updated") .action(msg.getType().equals(DataConstants.COMMENT_CREATED) ? "added" : "updated")
.userEmail(trigger.getMsg().getMetaData().getValue("userEmail")) .userEmail(msg.getMetaData().getValue("userEmail"))
.userFirstName(trigger.getMsg().getMetaData().getValue("userFirstName")) .userFirstName(msg.getMetaData().getValue("userFirstName"))
.userLastName(trigger.getMsg().getMetaData().getValue("userLastName")) .userLastName(msg.getMetaData().getValue("userLastName"))
.alarmId(alarmInfo.getUuidId()) .alarmId(alarmInfo.getUuidId())
.alarmType(alarmInfo.getType()) .alarmType(alarmInfo.getType())
.alarmOriginator(alarmInfo.getOriginator()) .alarmOriginator(alarmInfo.getOriginator())

8
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.notification.rule.trigger;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo; import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo;
@ -25,11 +26,10 @@ import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotific
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.ClearRule; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.ClearRule;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.msg.notification.trigger.AlarmTrigger; import org.thingsboard.server.common.msg.notification.trigger.AlarmTrigger;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty; import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service @Service
public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<AlarmTrigger, AlarmNotificationRuleTriggerConfig> { public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<AlarmTrigger, AlarmNotificationRuleTriggerConfig> {
@ -85,11 +85,11 @@ public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<A
} }
private boolean severityMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) { private boolean severityMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) {
return isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity()); return emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity());
} }
private boolean typeMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) { private boolean typeMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) {
return isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType()); return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType());
} }
@Override @Override

6
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ApiUsageLimitTriggerProcessor.java

@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.notification.rule.trigger.Notification
import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger; import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import static org.apache.commons.collections.CollectionUtils.isEmpty; import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ -34,8 +34,8 @@ public class ApiUsageLimitTriggerProcessor implements NotificationRuleTriggerPro
@Override @Override
public boolean matchesFilter(ApiUsageLimitTrigger trigger, ApiUsageLimitNotificationRuleTriggerConfig triggerConfig) { public boolean matchesFilter(ApiUsageLimitTrigger trigger, ApiUsageLimitNotificationRuleTriggerConfig triggerConfig) {
return (isEmpty(triggerConfig.getApiFeatures()) || triggerConfig.getApiFeatures().contains(trigger.getState().getApiFeature())) && return emptyOrContains(triggerConfig.getApiFeatures(), trigger.getState().getApiFeature()) &&
(isEmpty(triggerConfig.getNotifyOn()) || triggerConfig.getNotifyOn().contains(trigger.getStatus())); emptyOrContains(triggerConfig.getNotifyOn(), trigger.getStatus());
} }
@Override @Override

73
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java

@ -16,78 +16,47 @@
package org.thingsboard.server.service.notification.rule.trigger; package org.thingsboard.server.service.notification.rule.trigger;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo; import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.notification.trigger.EntityActionTrigger;
import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger;
import java.util.Optional; import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
import java.util.Set;
import java.util.UUID;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
@Service @Service
public class EntityActionTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<EntityActionNotificationRuleTriggerConfig> { public class EntityActionTriggerProcessor implements NotificationRuleTriggerProcessor<EntityActionTrigger, EntityActionNotificationRuleTriggerConfig> {
@Override @Override
public boolean matchesFilter(RuleEngineMsgTrigger trigger, EntityActionNotificationRuleTriggerConfig triggerConfig) { public boolean matchesFilter(EntityActionTrigger trigger, EntityActionNotificationRuleTriggerConfig triggerConfig) {
String msgType = trigger.getMsg().getType(); return ((trigger.getActionType() == ActionType.ADDED && triggerConfig.isCreated())
if (msgType.equals(DataConstants.ENTITY_CREATED)) { || (trigger.getActionType() == ActionType.UPDATED && triggerConfig.isUpdated())
if (!triggerConfig.isCreated()) { || (trigger.getActionType() == ActionType.DELETED && triggerConfig.isDeleted()))
return false; && emptyOrContains(triggerConfig.getEntityTypes(), trigger.getEntityId().getEntityType());
}
} else if (msgType.equals(DataConstants.ENTITY_UPDATED)) {
if (!triggerConfig.isUpdated()) {
return false;
}
} else if (msgType.equals(DataConstants.ENTITY_DELETED)) {
if (!triggerConfig.isDeleted()) {
return false;
}
} else {
return false;
}
return isEmpty(triggerConfig.getEntityTypes()) || triggerConfig.getEntityTypes().contains(getEntityType(trigger.getMsg()));
} }
@Override @Override
public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) { public RuleOriginatedNotificationInfo constructNotificationInfo(EntityActionTrigger trigger) {
TbMsg msg = trigger.getMsg();
String msgType = msg.getType();
ActionType actionType = msgType.equals(DataConstants.ENTITY_CREATED) ? ActionType.ADDED :
msgType.equals(DataConstants.ENTITY_UPDATED) ? ActionType.UPDATED :
msgType.equals(DataConstants.ENTITY_DELETED) ? ActionType.DELETED : null;
return EntityActionNotificationInfo.builder() return EntityActionNotificationInfo.builder()
.entityId(msg.getOriginator()) .entityId(trigger.getEntityId())
.entityName(msg.getMetaData().getValue("entityName")) .entityName(trigger.getEntity().getName())
.actionType(actionType) .actionType(trigger.getActionType())
.userId(UUID.fromString(msg.getMetaData().getValue("userId"))) .userId(trigger.getUser().getUuidId())
.userEmail(trigger.getMsg().getMetaData().getValue("userEmail")) .userTitle(trigger.getUser().getTitle())
.userFirstName(trigger.getMsg().getMetaData().getValue("userFirstName")) .userEmail(trigger.getUser().getEmail())
.userLastName(trigger.getMsg().getMetaData().getValue("userLastName")) .userFirstName(trigger.getUser().getFirstName())
.entityCustomerId(msg.getCustomerId()) .userLastName(trigger.getUser().getLastName())
.entityCustomerId(trigger.getEntity() instanceof HasCustomerId ?
((HasCustomerId) trigger.getEntity()).getCustomerId() :
trigger.getUser().getCustomerId())
.build(); .build();
} }
private static EntityType getEntityType(TbMsg msg) {
return Optional.ofNullable(msg.getMetaData().getValue("entityType"))
.map(EntityType::valueOf).orElse(null);
}
@Override @Override
public NotificationRuleTriggerType getTriggerType() { public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.ENTITY_ACTION; return NotificationRuleTriggerType.ENTITY_ACTION;
} }
@Override
public Set<String> getSupportedMsgTypes() {
return Set.of(DataConstants.ENTITY_CREATED, DataConstants.ENTITY_UPDATED, DataConstants.ENTITY_DELETED);
}
} }

6
application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java

@ -18,13 +18,13 @@ package org.thingsboard.server.service.queue;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.exception.TenantNotFoundException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.discovery.TenantRoutingInfo; import org.thingsboard.server.queue.discovery.TenantRoutingInfo;
import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; import org.thingsboard.server.queue.discovery.TenantRoutingInfoService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
@Slf4j @Slf4j
@Service @Service
@ -46,7 +46,7 @@ public class DefaultTenantRoutingInfoService implements TenantRoutingInfoService
if (tenantProfile != null) { if (tenantProfile != null) {
return new TenantRoutingInfo(tenantId, tenantProfile.isIsolatedTbRuleEngine()); return new TenantRoutingInfo(tenantId, tenantProfile.isIsolatedTbRuleEngine());
} else { } else {
throw new RuntimeException("Tenant not found!"); throw new TenantNotFoundException(tenantId);
} }
} }
} }

23
application/src/main/java/org/thingsboard/server/service/slack/DefaultSlackService.java

@ -28,7 +28,6 @@ import com.slack.api.methods.response.conversations.ConversationsListResponse;
import com.slack.api.methods.response.users.UsersListResponse; import com.slack.api.methods.response.users.UsersListResponse;
import com.slack.api.model.ConversationType; import com.slack.api.model.ConversationType;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -44,8 +43,6 @@ import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class DefaultSlackService implements SlackService { public class DefaultSlackService implements SlackService {
@ -81,15 +78,11 @@ public class DefaultSlackService implements SlackService {
.filter(user -> !user.isDeleted() && !user.isStranger() && !user.isBot()) .filter(user -> !user.isDeleted() && !user.isStranger() && !user.isBot())
.map(user -> { .map(user -> {
SlackConversation conversation = new SlackConversation(); SlackConversation conversation = new SlackConversation();
conversation.setType(conversationType);
conversation.setId(user.getId()); conversation.setId(user.getId());
conversation.setShortName(user.getName()); conversation.setName(user.getName());
conversation.setWholeName(user.getProfile() != null ? user.getProfile().getRealNameNormalized() : user.getRealName()); conversation.setWholeName(user.getProfile() != null ? user.getProfile().getRealNameNormalized() : user.getRealName());
conversation.setEmail(user.getProfile() != null ? user.getProfile().getEmail() : null); conversation.setEmail(user.getProfile() != null ? user.getProfile().getEmail() : null);
String title = "@" + conversation.getShortName();
if (isNotEmpty(conversation.getWholeName()) && !conversation.getWholeName().equals(conversation.getShortName())) {
title += " (" + conversation.getWholeName() + ")";
}
conversation.setTitle(title);
return conversation; return conversation;
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -107,10 +100,10 @@ public class DefaultSlackService implements SlackService {
.filter(channel -> !channel.isArchived()) .filter(channel -> !channel.isArchived())
.map(channel -> { .map(channel -> {
SlackConversation conversation = new SlackConversation(); SlackConversation conversation = new SlackConversation();
conversation.setType(conversationType);
conversation.setId(channel.getId()); conversation.setId(channel.getId());
conversation.setShortName(channel.getName()); conversation.setName(channel.getName());
conversation.setWholeName(channel.getNameNormalized()); conversation.setWholeName(channel.getNameNormalized());
conversation.setTitle("#" + channel.getName());
return conversation; return conversation;
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -118,14 +111,6 @@ public class DefaultSlackService implements SlackService {
}); });
} }
@Override
public SlackConversation findConversation(TenantId tenantId, String token, SlackConversationType conversationType, String namePattern) {
List<SlackConversation> conversations = listConversations(tenantId, token, conversationType);
return conversations.stream()
.filter(conversation -> StringUtils.containsIgnoreCase(conversation.getTitle(), namePattern))
.findFirst().orElse(null);
}
@Override @Override
public String getToken(TenantId tenantId) { public String getToken(TenantId tenantId) {
NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId); NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId);

4
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.exception.TenantNotFoundException;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -453,6 +454,9 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
updateInactivityStateIfExpired(ts, deviceId, stateData); updateInactivityStateIfExpired(ts, deviceId, stateData);
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}] Failed to update inactivity state", deviceId, e); log.warn("[{}] Failed to update inactivity state", deviceId, e);
if (e instanceof TenantNotFoundException) {
continue;
}
} }
Pair<AtomicInteger, AtomicInteger> tenantDevicesActivity = devicesActivity.computeIfAbsent(stateData.getTenantId(), Pair<AtomicInteger, AtomicInteger> tenantDevicesActivity = devicesActivity.computeIfAbsent(stateData.getTenantId(),
tenantId -> Pair.of(new AtomicInteger(), new AtomicInteger())); tenantId -> Pair.of(new AtomicInteger(), new AtomicInteger()));

11
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmModificationRequest; import org.thingsboard.server.common.data.alarm.AlarmModificationRequest;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
@ -189,6 +190,16 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
return alarmService.findCustomerAlarms(tenantId, customerId, query); return alarmService.findCustomerAlarms(tenantId, customerId, query);
} }
@Override
public ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) {
return alarmService.findAlarmsV2(tenantId, query);
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
return alarmService.findCustomerAlarmsV2(tenantId, customerId, query);
}
@Override @Override
public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) { public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) {
return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus, assigneeId); return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus, assigneeId);

31
application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java

@ -35,6 +35,10 @@ import org.thingsboard.server.common.data.notification.NotificationRequestConfig
import org.thingsboard.server.common.data.notification.NotificationRequestInfo; import org.thingsboard.server.common.data.notification.NotificationRequestInfo;
import org.thingsboard.server.common.data.notification.NotificationRequestStats; import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.rule.DefaultNotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
@ -52,6 +56,7 @@ import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -195,6 +200,32 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
new PageLink(limit, 0), unreadOnly).getData(); new PageLink(limit, 0), unreadOnly).getData();
} }
protected NotificationRule createNotificationRule(NotificationRuleTriggerConfig triggerConfig, String subject, String text, NotificationTargetId... targets) {
NotificationTemplate template = createNotificationTemplate(NotificationType.valueOf(triggerConfig.getTriggerType().toString()), subject, text, NotificationDeliveryMethod.WEB);
NotificationRule rule = new NotificationRule();
rule.setName(triggerConfig.getTriggerType() + " [" + Arrays.toString(targets) + "]");
rule.setTemplateId(template.getId());
rule.setTriggerType(triggerConfig.getTriggerType());
rule.setTriggerConfig(triggerConfig);
DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig();
recipientsConfig.setTriggerType(triggerConfig.getTriggerType());
recipientsConfig.setTargets(DaoUtil.toUUIDs(List.of(targets)));
rule.setRecipientsConfig(recipientsConfig);
return saveNotificationRule(rule);
}
protected NotificationRule saveNotificationRule(NotificationRule notificationRule) {
return doPost("/api/notification/rule", notificationRule, NotificationRule.class);
}
protected PageData<NotificationRuleInfo> findNotificationRules() throws Exception {
PageLink pageLink = new PageLink(10);
return doGetTypedWithPageLink("/api/notification/rules?", new TypeReference<PageData<NotificationRuleInfo>>() {}, pageLink);
}
@Override @Override
protected NotificationApiWsClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException { protected NotificationApiWsClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException {
NotificationApiWsClient wsClient = new NotificationApiWsClient(WS_URL + wsPort, token); NotificationApiWsClient wsClient = new NotificationApiWsClient(WS_URL + wsPort, token);

8
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -362,13 +362,14 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
SlackNotificationTargetConfig slackTargetConfig = new SlackNotificationTargetConfig(); SlackNotificationTargetConfig slackTargetConfig = new SlackNotificationTargetConfig();
slackTargetConfig.setConversationType(SlackConversationType.DIRECT); slackTargetConfig.setConversationType(SlackConversationType.DIRECT);
SlackConversation slackConversation = new SlackConversation(); SlackConversation slackConversation = new SlackConversation();
slackConversation.setType(SlackConversationType.DIRECT);
slackConversation.setId("U1234567"); slackConversation.setId("U1234567");
slackConversation.setTitle("@jdoe (John Doe)"); slackConversation.setName("jdoe");
slackConversation.setWholeName("John Doe"); slackConversation.setWholeName("John Doe");
slackTargetConfig.setConversation(slackConversation); slackTargetConfig.setConversation(slackConversation);
slackTarget.setConfiguration(slackTargetConfig); slackTarget.setConfiguration(slackTargetConfig);
slackTarget = saveNotificationTarget(slackTarget); slackTarget = saveNotificationTarget(slackTarget);
recipients.add(slackConversation.getTitle()); recipients.add("@" + slackConversation.getWholeName());
NotificationTemplate notificationTemplate = new NotificationTemplate(); NotificationTemplate notificationTemplate = new NotificationTemplate();
notificationTemplate.setNotificationType(NotificationType.GENERAL); notificationTemplate.setNotificationType(NotificationType.GENERAL);
@ -500,8 +501,9 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
notificationTarget.setName(conversationName + " in Slack"); notificationTarget.setName(conversationName + " in Slack");
SlackNotificationTargetConfig targetConfig = new SlackNotificationTargetConfig(); SlackNotificationTargetConfig targetConfig = new SlackNotificationTargetConfig();
targetConfig.setConversation(SlackConversation.builder() targetConfig.setConversation(SlackConversation.builder()
.type(SlackConversationType.DIRECT)
.id(conversationId) .id(conversationId)
.title(conversationName) .name(conversationName)
.build()); .build());
notificationTarget.setConfiguration(targetConfig); notificationTarget.setConfiguration(targetConfig);
notificationTarget = saveNotificationTarget(notificationTarget); notificationTarget = saveNotificationTarget(notificationTarget);

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

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.service.notification; package org.thingsboard.server.service.notification;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.BooleanNode; import com.fasterxml.jackson.databind.node.BooleanNode;
import org.junit.Before; import org.junit.Before;
@ -43,7 +42,6 @@ import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType;
import org.thingsboard.server.common.data.device.profile.AlarmRule; import org.thingsboard.server.common.data.device.profile.AlarmRule;
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.Notification;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
@ -59,7 +57,6 @@ import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotific
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction;
import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
@ -73,7 +70,6 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
@ -84,7 +80,6 @@ import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileServ
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -436,7 +431,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
String name = "device " + i; String name = "device " + i;
createDevice(name, name); createDevice(name, name);
} }
await().atMost(5, TimeUnit.SECONDS) await().atMost(30, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
assertThat(getMyNotifications(false, 100)).size().isEqualTo(notificationRequestsLimit); assertThat(getMyNotifications(false, 100)).size().isEqualTo(notificationRequestsLimit);
}); });
@ -474,21 +469,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
}, (notification, r) -> check.accept(notification)); }, (notification, r) -> check.accept(notification));
} }
private NotificationRule createNotificationRule(NotificationRuleTriggerConfig triggerConfig, String subject, String text, NotificationTargetId... targets) { private PageData<NotificationRequestInfo> findNotificationRequests(EntityType originatorType) {
NotificationTemplate template = createNotificationTemplate(NotificationType.valueOf(triggerConfig.getTriggerType().toString()), subject, text, NotificationDeliveryMethod.WEB); return notificationRequestService.findNotificationRequestsInfosByTenantIdAndOriginatorType(tenantId, originatorType, new PageLink(100));
NotificationRule rule = new NotificationRule();
rule.setName(triggerConfig.getTriggerType() + " [" + Arrays.toString(targets) + "]");
rule.setTemplateId(template.getId());
rule.setTriggerType(triggerConfig.getTriggerType());
rule.setTriggerConfig(triggerConfig);
DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig();
recipientsConfig.setTriggerType(triggerConfig.getTriggerType());
recipientsConfig.setTargets(DaoUtil.toUUIDs(List.of(targets)));
rule.setRecipientsConfig(recipientsConfig);
return saveNotificationRule(rule);
} }
private DeviceProfile createDeviceProfileWithAlarmRules(String alarmType) { private DeviceProfile createDeviceProfileWithAlarmRules(String alarmType) {
@ -526,17 +508,4 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
return deviceProfile; return deviceProfile;
} }
private NotificationRule saveNotificationRule(NotificationRule notificationRule) {
return doPost("/api/notification/rule", notificationRule, NotificationRule.class);
}
private PageData<NotificationRuleInfo> findNotificationRules() throws Exception {
PageLink pageLink = new PageLink(10);
return doGetTypedWithPageLink("/api/notification/rules?", new TypeReference<PageData<NotificationRuleInfo>>() {}, pageLink);
}
private PageData<NotificationRequestInfo> findNotificationRequests(EntityType originatorType) {
return notificationRequestService.findNotificationRequestsInfosByTenantIdAndOriginatorType(tenantId, originatorType, new PageLink(100));
}
} }

25
application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java

@ -23,6 +23,8 @@ import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.ResultMatcher;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter;
@ -30,7 +32,6 @@ import org.thingsboard.server.common.data.notification.targets.platform.Platform
import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.notification.NotificationTargetDao; import org.thingsboard.server.dao.notification.NotificationTargetDao;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
@ -41,7 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest @DaoSqlTest
public class NotificationTargetApiTest extends AbstractControllerTest { public class NotificationTargetApiTest extends AbstractNotificationApiTest {
@Autowired @Autowired
private NotificationTargetDao notificationTargetDao; private NotificationTargetDao notificationTargetDao;
@ -148,6 +149,26 @@ public class NotificationTargetApiTest extends AbstractControllerTest {
assertThat(notificationTargetDao.findByTenantIdAndPageLink(differentTenantId, new PageLink(10)).getData()).isEmpty(); assertThat(notificationTargetDao.findByTenantIdAndPageLink(differentTenantId, new PageLink(10)).getData()).isEmpty();
} }
@Test
public void whenDeletingTargetUsedByRule_thenReturnError() throws Exception {
NotificationTarget target = createNotificationTarget(tenantAdminUserId);
createNotificationRule(new EntityActionNotificationRuleTriggerConfig(), "Test", "Test", target.getId());
String error = getErrorMessage(doDelete("/api/notification/target/" + target.getId())
.andExpect(status().isBadRequest()));
assertThat(error).containsIgnoringCase("used in notification rule");
}
@Test
public void whenDeletingTargetUsedByScheduledNotificationRequest_thenReturnError() throws Exception {
NotificationTarget target = createNotificationTarget(tenantAdminUserId);
submitNotificationRequest(target.getId(), "Test", 100, NotificationDeliveryMethod.WEB);
String error = getErrorMessage(doDelete("/api/notification/target/" + target.getId())
.andExpect(status().isBadRequest()));
assertThat(error).containsIgnoringCase("referenced by scheduled notification request");
}
private String saveAndGetError(NotificationTarget notificationTarget, ResultMatcher statusMatcher) throws Exception { private String saveAndGetError(NotificationTarget notificationTarget, ResultMatcher statusMatcher) throws Exception {
return getErrorMessage(save(notificationTarget, statusMatcher)); return getErrorMessage(save(notificationTarget, statusMatcher));
} }

13
application/src/test/java/org/thingsboard/server/service/stats/DevicesStatisticsTest.java

@ -28,6 +28,7 @@ import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.state.DeviceStateService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -35,17 +36,14 @@ import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest @DaoSqlTest
@TestPropertySource(properties = { @TestPropertySource(properties = {
"usage.stats.report.enabled=true", "usage.stats.report.enabled=true",
"transport.http.enabled=true",
"usage.stats.report.interval=2", "usage.stats.report.interval=2",
"usage.stats.gauge_report_interval=1", "usage.stats.gauge_report_interval=1",
"state.defaultStateCheckIntervalInSec=3", "state.defaultStateCheckIntervalInSec=3",
"state.defaultInactivityTimeoutInSec=10" "state.defaultInactivityTimeoutInSec=10"
}) })
public class DevicesStatisticsTest extends AbstractControllerTest { public class DevicesStatisticsTest extends AbstractControllerTest {
@ -53,6 +51,8 @@ public class DevicesStatisticsTest extends AbstractControllerTest {
private TbApiUsageStateService apiUsageStateService; private TbApiUsageStateService apiUsageStateService;
@Autowired @Autowired
private TimeseriesService timeseriesService; private TimeseriesService timeseriesService;
@Autowired
private DeviceStateService deviceStateService;
private ApiUsageStateId apiUsageStateId; private ApiUsageStateId apiUsageStateId;
@ -86,7 +86,7 @@ public class DevicesStatisticsTest extends AbstractControllerTest {
}); });
for (Device device : activeDevices) { for (Device device : activeDevices) {
postTelemetry(device.getName(), "{\"dp\":1}"); deviceStateService.onDeviceActivity(tenantId, device.getId(), System.currentTimeMillis());
} }
await().atMost(40, TimeUnit.SECONDS) await().atMost(40, TimeUnit.SECONDS)
@ -102,9 +102,4 @@ public class DevicesStatisticsTest extends AbstractControllerTest {
.get().stream().findFirst().flatMap(KvEntry::getLongValue).orElse(null); .get().stream().findFirst().flatMap(KvEntry::getLongValue).orElse(null);
} }
@SneakyThrows
private void postTelemetry(String accessToken, String json) {
doPost("/api/v1/" + accessToken + "/telemetry", json, new String[0]).andExpect(status().isOk());
}
} }

5
common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
@ -105,6 +106,10 @@ public interface AlarmService extends EntityDaoService {
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus,
AlarmStatus alarmStatus, String assigneeId); AlarmStatus alarmStatus, String assigneeId);

6
common/data/src/main/java/org/thingsboard/server/common/data/User.java

@ -74,7 +74,7 @@ public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements H
@ApiModelProperty(position = 1, value = "JSON object with the User Id. " + @ApiModelProperty(position = 1, value = "JSON object with the User Id. " +
"Specify this field to update the device. " + "Specify this field to update the device. " +
"Referencing non-existing User Id will cause error. " + "Referencing non-existing User Id will cause error. " +
"Omit this field to create new customer." ) "Omit this field to create new customer.")
@Override @Override
public UserId getId() { public UserId getId() {
return super.getId(); return super.getId();
@ -169,6 +169,10 @@ public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements H
@JsonIgnore @JsonIgnore
public String getTitle() { public String getTitle() {
return getTitle(email, firstName, lastName);
}
public static String getTitle(String email, String firstName, String lastName) {
String title = ""; String title = "";
if (isNotEmpty(firstName)) { if (isNotEmpty(firstName)) {
title += firstName; title += firstName;

39
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQueryV2.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.alarm;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.TimePageLink;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
public class AlarmQueryV2 {
private EntityId affectedEntityId;
private TimePageLink pageLink;
private List<String> typeList;
private List<AlarmSearchStatus> statusList;
private List<AlarmSeverity> severityList;
private UserId assigneeId;
}

31
common/data/src/main/java/org/thingsboard/server/common/data/exception/TenantNotFoundException.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.exception;
import lombok.Getter;
import org.thingsboard.server.common.data.id.TenantId;
public class TenantNotFoundException extends RuntimeException {
@Getter
private final TenantId tenantId;
public TenantNotFoundException(TenantId tenantId) {
super("Tenant with id " + tenantId + " not found");
this.tenantId = tenantId;
}
}

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

@ -27,6 +27,6 @@ public enum NotificationType {
NEW_PLATFORM_VERSION, NEW_PLATFORM_VERSION,
ENTITIES_LIMIT, ENTITIES_LIMIT,
API_USAGE_LIMIT, API_USAGE_LIMIT,
RULE_ENGINE RULE_NODE
} }

3
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java

@ -19,6 +19,7 @@ import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -59,10 +60,12 @@ public class AlarmAssignmentNotificationInfo implements RuleOriginatedNotificati
public Map<String, String> getTemplateData() { public Map<String, String> getTemplateData() {
return mapOf( return mapOf(
"action", action, "action", action,
"assigneeTitle", User.getTitle(assigneeEmail, assigneeFirstName, assigneeLastName),
"assigneeFirstName", assigneeFirstName, "assigneeFirstName", assigneeFirstName,
"assigneeLastName", assigneeLastName, "assigneeLastName", assigneeLastName,
"assigneeEmail", assigneeEmail, "assigneeEmail", assigneeEmail,
"assigneeId", assigneeId != null ? assigneeId.toString() : null, "assigneeId", assigneeId != null ? assigneeId.toString() : null,
"userTitle", User.getTitle(userEmail, userFirstName, userLastName),
"userEmail", userEmail, "userEmail", userEmail,
"userFirstName", userFirstName, "userFirstName", userFirstName,
"userLastName", userLastName, "userLastName", userLastName,

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java

@ -19,6 +19,7 @@ import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -55,6 +56,7 @@ public class AlarmCommentNotificationInfo implements RuleOriginatedNotificationI
return mapOf( return mapOf(
"comment", comment, "comment", comment,
"action", action, "action", action,
"userTitle", User.getTitle(userEmail, userFirstName, userLastName),
"userEmail", userEmail, "userEmail", userEmail,
"userFirstName", userFirstName, "userFirstName", userFirstName,
"userLastName", userLastName, "userLastName", userLastName,

3
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntityActionNotificationInfo.java

@ -19,6 +19,7 @@ import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
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;
@ -40,6 +41,7 @@ public class EntityActionNotificationInfo implements RuleOriginatedNotificationI
private CustomerId entityCustomerId; private CustomerId entityCustomerId;
private UUID userId; private UUID userId;
private String userTitle;
private String userEmail; private String userEmail;
private String userFirstName; private String userFirstName;
private String userLastName; private String userLastName;
@ -52,6 +54,7 @@ public class EntityActionNotificationInfo implements RuleOriginatedNotificationI
"entityName", entityName, "entityName", entityName,
"actionType", actionType.name().toLowerCase(), "actionType", actionType.name().toLowerCase(),
"userId", userId.toString(), "userId", userId.toString(),
"userTitle", userTitle,
"userEmail", userEmail, "userEmail", userEmail,
"userFirstName", userFirstName, "userFirstName", userFirstName,
"userLastName", userLastName "userLastName", userLastName

24
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackConversation.java

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data.notification.targets.slack; package org.thingsboard.server.common.data.notification.targets.slack;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -24,6 +25,7 @@ import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; import org.thingsboard.server.common.data.notification.targets.NotificationRecipient;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import static org.apache.commons.lang3.StringUtils.isEmpty; import static org.apache.commons.lang3.StringUtils.isEmpty;
@ -31,23 +33,34 @@ import static org.apache.commons.lang3.StringUtils.isEmpty;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Builder @Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public class SlackConversation implements NotificationRecipient { public class SlackConversation implements NotificationRecipient {
@NotNull
private SlackConversationType type;
@NotEmpty @NotEmpty
private String id; private String id;
@NotEmpty @NotEmpty
private String title; private String name;
private String shortName;
private String wholeName; private String wholeName;
private String email; private String email;
@Override
public String getTitle() {
if (type == SlackConversationType.DIRECT) {
return StringUtils.defaultIfEmpty(wholeName, name);
} else {
return name;
}
}
@JsonIgnore @JsonIgnore
@Override @Override
public String getFirstName() { public String getFirstName() {
String firstName = StringUtils.contains(wholeName, " ") ? wholeName.split(" ")[0] : wholeName; String firstName = StringUtils.contains(wholeName, " ") ? wholeName.split(" ")[0] : wholeName;
if (isEmpty(firstName)) { if (isEmpty(firstName)) {
firstName = shortName; firstName = name;
} }
return firstName; return firstName;
} }
@ -58,4 +71,9 @@ public class SlackConversation implements NotificationRecipient {
return StringUtils.contains(wholeName, " ") ? wholeName.split(" ")[1] : null; return StringUtils.contains(wholeName, " ") ? wholeName.split(" ")[1] : null;
} }
@JsonIgnore
public String getPointer() {
return type == SlackConversationType.DIRECT ? "@" : "#";
}
} }

29
common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentClusteringMode.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.plugin;
/**
* The main idea to use this - it's adding the ability to start rule nodes in singleton mode in cluster setup
* (singleton rule node will start in only one Rule Engine instance)
* USER_PREFERENCE - user has ability to configure clustering mode (enable/disable singleton mode in rule node config)
* ENABLE - user doesn't have ability to configure clustering mode (singleton mode is always FALSE in rule node config)
* SINGLETON - user doesn't have ability to configure clustering mode (singleton mode is always TRUE in rule node config)
*/
public enum ComponentClusteringMode {
USER_PREFERENCE,
ENABLED,
SINGLETON
}

10
common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java

@ -36,15 +36,17 @@ public class ComponentDescriptor extends SearchTextBased<ComponentDescriptorId>
@Getter @Setter private ComponentType type; @Getter @Setter private ComponentType type;
@ApiModelProperty(position = 4, value = "Scope of the Rule Node. Always set to 'TENANT', since no rule chains on the 'SYSTEM' level yet.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, allowableValues = "TENANT", example = "TENANT") @ApiModelProperty(position = 4, value = "Scope of the Rule Node. Always set to 'TENANT', since no rule chains on the 'SYSTEM' level yet.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, allowableValues = "TENANT", example = "TENANT")
@Getter @Setter private ComponentScope scope; @Getter @Setter private ComponentScope scope;
@ApiModelProperty(position = 5, value = "Clustering mode of the RuleNode. This mode represents the ability to start Rule Node in multiple microservices.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, allowableValues = "USER_PREFERENCE, ENABLED, SINGLETON", example = "ENABLED")
@Getter @Setter private ComponentClusteringMode clusteringMode;
@Length(fieldName = "name") @Length(fieldName = "name")
@ApiModelProperty(position = 5, value = "Name of the Rule Node. Taken from the @RuleNode annotation.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, example = "Custom Rule Node") @ApiModelProperty(position = 6, value = "Name of the Rule Node. Taken from the @RuleNode annotation.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, example = "Custom Rule Node")
@Getter @Setter private String name; @Getter @Setter private String name;
@ApiModelProperty(position = 6, value = "Full name of the Java class that implements the Rule Engine Node interface.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, example = "com.mycompany.CustomRuleNode") @ApiModelProperty(position = 7, value = "Full name of the Java class that implements the Rule Engine Node interface.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, example = "com.mycompany.CustomRuleNode")
@Getter @Setter private String clazz; @Getter @Setter private String clazz;
@ApiModelProperty(position = 7, value = "Complex JSON object that represents the Rule Node configuration.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @ApiModelProperty(position = 8, value = "Complex JSON object that represents the Rule Node configuration.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
@Getter @Setter private transient JsonNode configurationDescriptor; @Getter @Setter private transient JsonNode configurationDescriptor;
@Length(fieldName = "actions") @Length(fieldName = "actions")
@ApiModelProperty(position = 8, value = "Rule Node Actions. Deprecated. Always null.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @ApiModelProperty(position = 9, value = "Rule Node Actions. Deprecated. Always null.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
@Getter @Setter private String actions; @Getter @Setter private String actions;
public ComponentDescriptor() { public ComponentDescriptor() {

5
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java

@ -48,7 +48,9 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo<RuleNodeId> impl
private String name; private String name;
@ApiModelProperty(position = 6, value = "Enable/disable debug. ", example = "false") @ApiModelProperty(position = 6, value = "Enable/disable debug. ", example = "false")
private boolean debugMode; private boolean debugMode;
@ApiModelProperty(position = 7, value = "JSON with the rule node configuration. Structure depends on the rule node implementation.", dataType = "com.fasterxml.jackson.databind.JsonNode") @ApiModelProperty(position = 7, value = "Enable/disable singleton mode. ", example = "false")
private boolean singletonMode;
@ApiModelProperty(position = 8, value = "JSON with the rule node configuration. Structure depends on the rule node implementation.", dataType = "com.fasterxml.jackson.databind.JsonNode")
private transient JsonNode configuration; private transient JsonNode configuration;
@JsonIgnore @JsonIgnore
private byte[] configurationBytes; private byte[] configurationBytes;
@ -69,6 +71,7 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo<RuleNodeId> impl
this.type = ruleNode.getType(); this.type = ruleNode.getType();
this.name = ruleNode.getName(); this.name = ruleNode.getName();
this.debugMode = ruleNode.isDebugMode(); this.debugMode = ruleNode.isDebugMode();
this.singletonMode = ruleNode.isSingletonMode();
this.setConfiguration(ruleNode.getConfiguration()); this.setConfiguration(ruleNode.getConfiguration());
this.externalId = ruleNode.getExternalId(); this.externalId = ruleNode.getExternalId();
} }

8
common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java

@ -15,8 +15,6 @@
*/ */
package org.thingsboard.server.common.data.util; package org.thingsboard.server.common.data.util;
import com.google.common.collect.ImmutableMap;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -63,8 +61,12 @@ public class CollectionsUtil {
return map; return map;
} }
public static <K, V> Map<K, V> unmodifiableMapOf(Object... kvs) { public static <K, V> Map<K, V> unmodifiableMapOf(Object... kvs) {
return Collections.unmodifiableMap(mapOf(kvs)); return Collections.unmodifiableMap(mapOf(kvs));
} }
public static <V> boolean emptyOrContains(Collection<V> collection, V element) {
return isEmpty(collection) || collection.contains(element);
}
} }

47
common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/EntityActionTrigger.java

@ -0,0 +1,47 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.msg.notification.trigger;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
@Data
@Builder
public class EntityActionTrigger implements NotificationRuleTrigger {
private final TenantId tenantId;
private final EntityId entityId;
private final HasName entity;
private final ActionType actionType;
private final User user;
@Override
public NotificationRuleTriggerType getType() {
return NotificationRuleTriggerType.ENTITY_ACTION;
}
@Override
public EntityId getOriginatorEntityId() {
return entityId;
}
}

3
common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java

@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.exception.TenantNotFoundException;
import org.thingsboard.server.common.data.id.EntityId; 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.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
@ -368,7 +369,7 @@ public class HashPartitionService implements PartitionService {
} }
} }
if (routingInfo == null) { if (routingInfo == null) {
throw new RuntimeException("Tenant not found!"); throw new TenantNotFoundException(tenantId);
} }
switch (serviceType) { switch (serviceType) {
case TB_RULE_ENGINE: case TB_RULE_ENGINE:

3
common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java

@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.exception.TenantNotFoundException;
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.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -122,6 +123,8 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, parent.getTenantId(), parent.getId()) TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, parent.getTenantId(), parent.getId())
.newByTopic(msgProducer.getDefaultTopic()); .newByTopic(msgProducer.getDefaultTopic());
msgProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg.build()), null); msgProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg.build()), null);
} catch (TenantNotFoundException e) {
log.debug("Couldn't report usage stats for non-existing tenant: {}", e.getTenantId());
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to report usage stats for tenant {}", parent.getTenantId(), e); log.warn("Failed to report usage stats for tenant {}", parent.getTenantId(), e);
} }

5
dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
@ -66,6 +67,10 @@ public interface AlarmDao extends Dao<Alarm> {
PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
PageData<AlarmInfo> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
PageData<AlarmInfo> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds); PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds);
Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, AlarmStatusFilter asf, String assigneeId); Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, AlarmStatusFilter asf, String assigneeId);

11
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmModificationRequest; import org.thingsboard.server.common.data.alarm.AlarmModificationRequest;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
@ -338,6 +339,16 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
return Futures.immediateFuture(alarmDao.findCustomerAlarms(tenantId, customerId, query)); return Futures.immediateFuture(alarmDao.findCustomerAlarms(tenantId, customerId, query));
} }
@Override
public ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) {
return Futures.immediateFuture(alarmDao.findAlarmsV2(tenantId, query));
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
return Futures.immediateFuture(alarmDao.findCustomerAlarmsV2(tenantId, customerId, query));
}
@Override @Override
public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus,
AlarmStatus alarmStatus, String assigneeId) { AlarmStatus alarmStatus, String assigneeId) {

2
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -403,6 +403,7 @@ public class ModelConstants {
public static final String COMPONENT_DESCRIPTOR_COLUMN_FAMILY_NAME = "component_descriptor"; public static final String COMPONENT_DESCRIPTOR_COLUMN_FAMILY_NAME = "component_descriptor";
public static final String COMPONENT_DESCRIPTOR_TYPE_PROPERTY = "type"; public static final String COMPONENT_DESCRIPTOR_TYPE_PROPERTY = "type";
public static final String COMPONENT_DESCRIPTOR_SCOPE_PROPERTY = "scope"; public static final String COMPONENT_DESCRIPTOR_SCOPE_PROPERTY = "scope";
public static final String COMPONENT_DESCRIPTOR_CLUSTERING_MODE_PROPERTY = "clustering_mode";
public static final String COMPONENT_DESCRIPTOR_NAME_PROPERTY = "name"; public static final String COMPONENT_DESCRIPTOR_NAME_PROPERTY = "name";
public static final String COMPONENT_DESCRIPTOR_CLASS_PROPERTY = "clazz"; public static final String COMPONENT_DESCRIPTOR_CLASS_PROPERTY = "clazz";
public static final String COMPONENT_DESCRIPTOR_CONFIGURATION_DESCRIPTOR_PROPERTY = "configuration_descriptor"; public static final String COMPONENT_DESCRIPTOR_CONFIGURATION_DESCRIPTOR_PROPERTY = "configuration_descriptor";
@ -446,6 +447,7 @@ public class ModelConstants {
public static final String EVENT_MESSAGE_COLUMN_NAME = "e_message"; public static final String EVENT_MESSAGE_COLUMN_NAME = "e_message";
public static final String DEBUG_MODE = "debug_mode"; public static final String DEBUG_MODE = "debug_mode";
public static final String SINGLETON_MODE = "singleton_mode";
/** /**
* Cassandra rule chain constants. * Cassandra rule chain constants.

7
dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java

@ -23,6 +23,7 @@ import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.id.ComponentDescriptorId; import org.thingsboard.server.common.data.id.ComponentDescriptorId;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentScope;
import org.thingsboard.server.common.data.plugin.ComponentClusteringMode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
@ -50,6 +51,10 @@ public class ComponentDescriptorEntity extends BaseSqlEntity<ComponentDescriptor
@Column(name = ModelConstants.COMPONENT_DESCRIPTOR_SCOPE_PROPERTY) @Column(name = ModelConstants.COMPONENT_DESCRIPTOR_SCOPE_PROPERTY)
private ComponentScope scope; private ComponentScope scope;
@Enumerated(EnumType.STRING)
@Column(name = ModelConstants.COMPONENT_DESCRIPTOR_CLUSTERING_MODE_PROPERTY)
private ComponentClusteringMode clusteringMode;
@Column(name = ModelConstants.COMPONENT_DESCRIPTOR_NAME_PROPERTY) @Column(name = ModelConstants.COMPONENT_DESCRIPTOR_NAME_PROPERTY)
private String name; private String name;
@ -77,6 +82,7 @@ public class ComponentDescriptorEntity extends BaseSqlEntity<ComponentDescriptor
this.actions = component.getActions(); this.actions = component.getActions();
this.type = component.getType(); this.type = component.getType();
this.scope = component.getScope(); this.scope = component.getScope();
this.clusteringMode = component.getClusteringMode();
this.name = component.getName(); this.name = component.getName();
this.clazz = component.getClazz(); this.clazz = component.getClazz();
this.configurationDescriptor = component.getConfigurationDescriptor(); this.configurationDescriptor = component.getConfigurationDescriptor();
@ -89,6 +95,7 @@ public class ComponentDescriptorEntity extends BaseSqlEntity<ComponentDescriptor
data.setCreatedTime(createdTime); data.setCreatedTime(createdTime);
data.setType(type); data.setType(type);
data.setScope(scope); data.setScope(scope);
data.setClusteringMode(clusteringMode);
data.setName(this.getName()); data.setName(this.getName());
data.setClazz(this.getClazz()); data.setClazz(this.getClazz());
data.setActions(this.getActions()); data.setActions(this.getActions());

5
dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java

@ -64,6 +64,9 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> implements SearchTex
@Column(name = ModelConstants.DEBUG_MODE) @Column(name = ModelConstants.DEBUG_MODE)
private boolean debugMode; private boolean debugMode;
@Column(name = ModelConstants.SINGLETON_MODE)
private boolean singletonMode;
@Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY)
private UUID externalId; private UUID externalId;
@ -81,6 +84,7 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> implements SearchTex
this.type = ruleNode.getType(); this.type = ruleNode.getType();
this.name = ruleNode.getName(); this.name = ruleNode.getName();
this.debugMode = ruleNode.isDebugMode(); this.debugMode = ruleNode.isDebugMode();
this.singletonMode = ruleNode.isSingletonMode();
this.searchText = ruleNode.getName(); this.searchText = ruleNode.getName();
this.configuration = ruleNode.getConfiguration(); this.configuration = ruleNode.getConfiguration();
this.additionalInfo = ruleNode.getAdditionalInfo(); this.additionalInfo = ruleNode.getAdditionalInfo();
@ -109,6 +113,7 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> implements SearchTex
ruleNode.setType(type); ruleNode.setType(type);
ruleNode.setName(name); ruleNode.setName(name);
ruleNode.setDebugMode(debugMode); ruleNode.setDebugMode(debugMode);
ruleNode.setSingletonMode(singletonMode);
ruleNode.setConfiguration(configuration); ruleNode.setConfiguration(configuration);
ruleNode.setAdditionalInfo(additionalInfo); ruleNode.setAdditionalInfo(additionalInfo);
if (externalId != null) { if (externalId != null) {

8
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java

@ -67,7 +67,7 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl
return notificationTargetDao.saveAndFlush(tenantId, notificationTarget); return notificationTargetDao.saveAndFlush(tenantId, notificationTarget);
} catch (Exception e) { } catch (Exception e) {
checkConstraintViolation(e, Map.of( checkConstraintViolation(e, Map.of(
"uq_notification_target_name", "Notification target with such name already exists" "uq_notification_target_name", "Recipients group with such name already exists"
)); ));
throw e; throw e;
} }
@ -109,7 +109,7 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl
List<User> users = ((UserListFilter) usersFilter).getUsersIds().stream() List<User> users = ((UserListFilter) usersFilter).getUsersIds().stream()
.limit(pageLink.getPageSize()) .limit(pageLink.getPageSize())
.map(UserId::new).map(userId -> userService.findUserById(tenantId, userId)) .map(UserId::new).map(userId -> userService.findUserById(tenantId, userId))
.collect(Collectors.toList()); .filter(Objects::nonNull).collect(Collectors.toList());
return new PageData<>(users, 1, users.size(), false); return new PageData<>(users, 1, users.size(), false);
} }
case CUSTOMER_USERS: { case CUSTOMER_USERS: {
@ -179,10 +179,10 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl
@Override @Override
public void deleteNotificationTargetById(TenantId tenantId, NotificationTargetId id) { public void deleteNotificationTargetById(TenantId tenantId, NotificationTargetId id) {
if (notificationRequestDao.existsByTenantIdAndStatusAndTargetId(tenantId, NotificationRequestStatus.SCHEDULED, id)) { if (notificationRequestDao.existsByTenantIdAndStatusAndTargetId(tenantId, NotificationRequestStatus.SCHEDULED, id)) {
throw new IllegalArgumentException("Notification target is referenced by scheduled notification request"); throw new IllegalArgumentException("Recipients group is referenced by scheduled notification request");
} }
if (notificationRuleDao.existsByTenantIdAndTargetId(tenantId, id)) { if (notificationRuleDao.existsByTenantIdAndTargetId(tenantId, id)) {
throw new IllegalArgumentException("Notification target is being used in notification rule"); throw new IllegalArgumentException("Recipients group is being used in notification rule");
} }
notificationTargetDao.removeById(tenantId, id.getId()); notificationTargetDao.removeById(tenantId, id.getId());
} }

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

@ -71,7 +71,7 @@ public class DefaultNotifications {
.type(NotificationType.ENTITIES_LIMIT) .type(NotificationType.ENTITIES_LIMIT)
.subject("${entityType}s limit will be reached soon for tenant ${tenantName}") .subject("${entityType}s limit will be reached soon for tenant ${tenantName}")
.text("${entityType}s usage: ${currentCount}/${limit} (${percents}%)") .text("${entityType}s usage: ${currentCount}/${limit} (${percents}%)")
.icon("warning").color("#D6D634") .icon("warning").color("#F9D916")
.rule(DefaultRule.builder() .rule(DefaultRule.builder()
.name("Entities count limit (sysadmin)") .name("Entities count limit (sysadmin)")
.triggerConfig(EntitiesLimitNotificationRuleTriggerConfig.builder() .triggerConfig(EntitiesLimitNotificationRuleTriggerConfig.builder()
@ -82,7 +82,7 @@ public class DefaultNotifications {
.build(); .build();
public static final DefaultNotification entitiesLimitForTenant = entitiesLimitForSysadmin.toBuilder() public static final DefaultNotification entitiesLimitForTenant = entitiesLimitForSysadmin.toBuilder()
.name("Entities count limit notification for tenant") .name("Entities count limit notification for tenant")
.subject("${entityType}s limit will be reached soon") .subject("WARNING: ${entityType}s limit will be reached soon")
.rule(entitiesLimitForSysadmin.getRule().toBuilder() .rule(entitiesLimitForSysadmin.getRule().toBuilder()
.name("Entities count limit") .name("Entities count limit")
.description("Send notification to tenant admins when count of entities of some type reached 80% threshold of the limit") .description("Send notification to tenant admins when count of entities of some type reached 80% threshold of the limit")
@ -93,7 +93,7 @@ public class DefaultNotifications {
.type(NotificationType.API_USAGE_LIMIT) .type(NotificationType.API_USAGE_LIMIT)
.subject("${feature} feature will be disabled soon for tenant ${tenantName}") .subject("${feature} feature will be disabled soon for tenant ${tenantName}")
.text("Usage: ${currentValue} out of ${limit} ${unitLabel}s") .text("Usage: ${currentValue} out of ${limit} ${unitLabel}s")
.icon("warning").color("#D6D634") .icon("warning").color("#F9D916")
.rule(DefaultRule.builder() .rule(DefaultRule.builder()
.name("API feature warning (sysadmin)") .name("API feature warning (sysadmin)")
.triggerConfig(ApiUsageLimitNotificationRuleTriggerConfig.builder() .triggerConfig(ApiUsageLimitNotificationRuleTriggerConfig.builder()
@ -105,7 +105,7 @@ public class DefaultNotifications {
.build(); .build();
public static final DefaultNotification apiFeatureWarningForTenant = apiFeatureWarningForSysadmin.toBuilder() public static final DefaultNotification apiFeatureWarningForTenant = apiFeatureWarningForSysadmin.toBuilder()
.name("API feature warning notification for tenant") .name("API feature warning notification for tenant")
.subject("${feature} feature will be disabled soon") .subject("WARNING: ${feature} feature will be disabled soon")
.rule(apiFeatureWarningForSysadmin.getRule().toBuilder() .rule(apiFeatureWarningForSysadmin.getRule().toBuilder()
.name("API feature warning") .name("API feature warning")
.description("Send notification to tenant admins on API feature usage WARNING state") .description("Send notification to tenant admins on API feature usage WARNING state")

29
dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java

@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.plugin.ComponentClusteringMode;
import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.NodeConnectionInfo;
@ -50,6 +51,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleChainUpdateResult; import org.thingsboard.server.common.data.rule.RuleChainUpdateResult;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.rule.RuleNodeUpdateResult; import org.thingsboard.server.common.data.rule.RuleNodeUpdateResult;
import org.thingsboard.server.common.data.util.ReflectionUtils;
import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.EntityCountService; import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
@ -154,6 +156,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
Map<RuleNodeId, Integer> ruleNodeIndexMap = new HashMap<>(); Map<RuleNodeId, Integer> ruleNodeIndexMap = new HashMap<>();
if (nodes != null) { if (nodes != null) {
for (RuleNode node : nodes) { for (RuleNode node : nodes) {
setSingletonMode(node);
if (node.getId() != null) { if (node.getId() != null) {
ruleNodeIndexMap.put(node.getId(), nodes.indexOf(node)); ruleNodeIndexMap.put(node.getId(), nodes.indexOf(node));
} else { } else {
@ -783,4 +786,30 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
} }
}; };
private void setSingletonMode(RuleNode ruleNode) {
boolean singletonMode;
try {
ComponentClusteringMode nodeConfigType = ReflectionUtils.getAnnotationProperty(ruleNode.getType(),
"org.thingsboard.rule.engine.api.RuleNode", "clusteringMode");
switch (nodeConfigType) {
case ENABLED:
singletonMode = false;
break;
case SINGLETON:
singletonMode = true;
break;
case USER_PREFERENCE:
default:
singletonMode = ruleNode.isSingletonMode();
break;
}
} catch (Exception e) {
log.warn("Failed to get clustering mode: {}", ExceptionUtils.getRootCauseMessage(e));
singletonMode = false;
}
ruleNode.setSingletonMode(singletonMode);
}
} }

4
dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java

@ -30,14 +30,12 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.util.ReflectionUtils; import org.thingsboard.server.common.data.util.ReflectionUtils;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.rule.RuleChainDao;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import java.util.HashMap; import java.util.HashMap;
@ -62,7 +60,7 @@ public class RuleChainDataValidator extends DataValidator<RuleChain> {
@Override @Override
protected void validateCreate(TenantId tenantId, RuleChain data) { protected void validateCreate(TenantId tenantId, RuleChain data) {
validateNumberOfEntitiesPerTenant(tenantId, EntityType.RULE_CHAIN); validateNumberOfEntitiesPerTenant(tenantId, EntityType.RULE_CHAIN);
} }
@Override @Override

133
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java

@ -90,6 +90,57 @@ public interface AlarmRepository extends JpaRepository<AlarmEntity, UUID> {
@Param("searchText") String searchText, @Param("searchText") String searchText,
Pageable pageable); Pageable pageable);
@Query(value = "SELECT a " +
"FROM AlarmInfoEntity a " +
"LEFT JOIN EntityAlarmEntity ea ON a.id = ea.alarmId " +
"WHERE a.tenantId = :tenantId " +
"AND ea.tenantId = :tenantId " +
"AND ea.entityId = :affectedEntityId " +
"AND ea.entityType = :affectedEntityType " +
"AND (:startTime IS NULL OR (a.createdTime >= :startTime AND ea.createdTime >= :startTime)) " +
"AND (:endTime IS NULL OR (a.createdTime <= :endTime AND ea.createdTime <= :endTime)) " +
"AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " +
"AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " +
"AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " +
"AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " +
"AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " +
"AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " +
" OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " +
" OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) "
,
countQuery = "" +
"SELECT count(a) " + //alarms with relations only
"FROM AlarmInfoEntity a " +
"LEFT JOIN EntityAlarmEntity ea ON a.id = ea.alarmId " +
"WHERE a.tenantId = :tenantId " +
"AND ea.tenantId = :tenantId " +
"AND ea.entityId = :affectedEntityId " +
"AND ea.entityType = :affectedEntityType " +
"AND (:startTime IS NULL OR (a.createdTime >= :startTime AND ea.createdTime >= :startTime)) " +
"AND (:endTime IS NULL OR (a.createdTime <= :endTime AND ea.createdTime <= :endTime)) " +
"AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " +
"AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " +
"AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " +
"AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " +
"AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " +
"AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " +
" OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " +
" OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ")
Page<AlarmInfoEntity> findAlarmsV2(@Param("tenantId") UUID tenantId,
@Param("affectedEntityId") UUID affectedEntityId,
@Param("affectedEntityType") String affectedEntityType,
@Param("startTime") Long startTime,
@Param("endTime") Long endTime,
@Param("alarmTypes") List<String> alarmTypes,
@Param("alarmSeverities") List<AlarmSeverity> alarmSeverities,
@Param("clearFilterEnabled") boolean clearFilterEnabled,
@Param("clearFilter") boolean clearFilter,
@Param("ackFilterEnabled") boolean ackFilterEnabled,
@Param("ackFilter") boolean ackFilter,
@Param("assigneeId") String assigneeId,
@Param("searchText") String searchText,
Pageable pageable);
@Query(value = "SELECT a " + @Query(value = "SELECT a " +
"FROM AlarmInfoEntity a " + "FROM AlarmInfoEntity a " +
"WHERE a.tenantId = :tenantId " + "WHERE a.tenantId = :tenantId " +
@ -124,6 +175,46 @@ public interface AlarmRepository extends JpaRepository<AlarmEntity, UUID> {
@Param("searchText") String searchText, @Param("searchText") String searchText,
Pageable pageable); Pageable pageable);
@Query(value = "SELECT a " +
"FROM AlarmInfoEntity a " +
"WHERE a.tenantId = :tenantId " +
"AND (:startTime IS NULL OR a.createdTime >= :startTime) " +
"AND (:endTime IS NULL OR a.createdTime <= :endTime) " +
"AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " +
"AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " +
"AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " +
"AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " +
"AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " +
"AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " +
" OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " +
" OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ",
countQuery = "" +
"SELECT count(a) " +
"FROM AlarmInfoEntity a " +
"WHERE a.tenantId = :tenantId " +
"AND (:startTime IS NULL OR a.createdTime >= :startTime) " +
"AND (:endTime IS NULL OR a.createdTime <= :endTime) " +
"AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " +
"AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " +
"AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " +
"AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " +
"AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " +
"AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " +
" OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " +
" OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ")
Page<AlarmInfoEntity> findAllAlarmsV2(@Param("tenantId") UUID tenantId,
@Param("startTime") Long startTime,
@Param("endTime") Long endTime,
@Param("alarmTypes") List<String> alarmTypes,
@Param("alarmSeverities") List<AlarmSeverity> alarmSeverities,
@Param("clearFilterEnabled") boolean clearFilterEnabled,
@Param("clearFilter") boolean clearFilter,
@Param("ackFilterEnabled") boolean ackFilterEnabled,
@Param("ackFilter") boolean ackFilter,
@Param("assigneeId") String assigneeId,
@Param("searchText") String searchText,
Pageable pageable);
@Query(value = "SELECT a " + @Query(value = "SELECT a " +
"FROM AlarmInfoEntity a " + "FROM AlarmInfoEntity a " +
"WHERE a.tenantId = :tenantId AND a.customerId = :customerId " + "WHERE a.tenantId = :tenantId AND a.customerId = :customerId " +
@ -160,6 +251,48 @@ public interface AlarmRepository extends JpaRepository<AlarmEntity, UUID> {
@Param("searchText") String searchText, @Param("searchText") String searchText,
Pageable pageable); Pageable pageable);
@Query(value = "SELECT a " +
"FROM AlarmInfoEntity a " +
"WHERE a.tenantId = :tenantId AND a.customerId = :customerId " +
"AND (:startTime IS NULL OR a.createdTime >= :startTime) " +
"AND (:endTime IS NULL OR a.createdTime <= :endTime) " +
"AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " +
"AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " +
"AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " +
"AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " +
"AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " +
"AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " +
" OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " +
" OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) "
,
countQuery = "" +
"SELECT count(a) " +
"FROM AlarmInfoEntity a " +
"WHERE a.tenantId = :tenantId AND a.customerId = :customerId " +
"AND (:startTime IS NULL OR a.createdTime >= :startTime) " +
"AND (:endTime IS NULL OR a.createdTime <= :endTime) " +
"AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " +
"AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " +
"AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " +
"AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " +
"AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " +
"AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " +
" OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " +
" OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ")
Page<AlarmInfoEntity> findCustomerAlarmsV2(@Param("tenantId") UUID tenantId,
@Param("customerId") UUID customerId,
@Param("startTime") Long startTime,
@Param("endTime") Long endTime,
@Param("alarmTypes") List<String> alarmTypes,
@Param("alarmSeverities") List<AlarmSeverity> alarmSeverities,
@Param("clearFilterEnabled") boolean clearFilterEnabled,
@Param("clearFilter") boolean clearFilter,
@Param("ackFilterEnabled") boolean ackFilterEnabled,
@Param("ackFilter") boolean ackFilter,
@Param("assigneeId") String assigneeId,
@Param("searchText") String searchText,
Pageable pageable);
@Query(value = "SELECT a.severity FROM AlarmEntity a " + @Query(value = "SELECT a.severity FROM AlarmEntity a " +
"LEFT JOIN EntityAlarmEntity ea ON a.id = ea.alarmId " + "LEFT JOIN EntityAlarmEntity ea ON a.id = ea.alarmId " +
"WHERE a.tenantId = :tenantId " + "WHERE a.tenantId = :tenantId " +

80
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java

@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo; import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
@ -200,6 +201,85 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
); );
} }
@Override
public PageData<AlarmInfo> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) {
log.trace("Try to find alarms by entity [{}], query [{}] and pageLink [{}]", query.getAffectedEntityId(), query, query.getPageLink());
EntityId affectedEntity = query.getAffectedEntityId();
List<String> typeList = query.getTypeList() != null && !query.getTypeList().isEmpty() ? query.getTypeList() : null;
List<AlarmSeverity> severityList = query.getSeverityList() != null && !query.getSeverityList().isEmpty() ? query.getSeverityList() : null;
AlarmStatusFilter asf = AlarmStatusFilter.from(query.getStatusList());
String assigneeId = null;
if (query.getAssigneeId() != null) {
assigneeId = query.getAssigneeId().toString();
}
if (affectedEntity != null) {
return DaoUtil.toPageData(
alarmRepository.findAlarmsV2(
tenantId.getId(),
affectedEntity.getId(),
affectedEntity.getEntityType().name(),
query.getPageLink().getStartTime(),
query.getPageLink().getEndTime(),
typeList,
severityList,
asf.hasClearFilter(),
asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(),
assigneeId,
Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink())
)
);
} else {
return DaoUtil.toPageData(
alarmRepository.findAllAlarmsV2(
tenantId.getId(),
query.getPageLink().getStartTime(),
query.getPageLink().getEndTime(),
typeList,
severityList,
asf.hasClearFilter(),
asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(),
assigneeId,
Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink())
)
);
}
}
@Override
public PageData<AlarmInfo> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
log.trace("Try to find customer alarms by query [{}] and pageLink [{}]", query, query.getPageLink());
List<String> typeList = query.getTypeList() != null && !query.getTypeList().isEmpty() ? query.getTypeList() : null;
List<AlarmSeverity> severityList = query.getSeverityList() != null && !query.getSeverityList().isEmpty() ? query.getSeverityList() : null;
AlarmStatusFilter asf = AlarmStatusFilter.from(query.getStatusList());
String assigneeId = null;
if (query.getAssigneeId() != null) {
assigneeId = query.getAssigneeId().toString();
}
return DaoUtil.toPageData(
alarmRepository.findCustomerAlarmsV2(
tenantId.getId(),
customerId.getId(),
query.getPageLink().getStartTime(),
query.getPageLink().getEndTime(),
typeList,
severityList,
asf.hasClearFilter(),
asf.hasClearFilter() && asf.getClearFilter(),
asf.hasAckFilter(),
asf.hasAckFilter() && asf.getAckFilter(),
assigneeId,
Objects.toString(query.getPageLink().getTextSearch(), ""),
DaoUtil.toPageable(query.getPageLink())
)
);
}
@Override @Override
public PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds) { public PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds) {
return alarmQueryRepository.findAlarmDataByQueryForEntities(tenantId, query, orderedEntityIds); return alarmQueryRepository.findAlarmDataByQueryForEntities(tenantId, query, orderedEntityIds);

3
dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java

@ -76,7 +76,8 @@ public abstract class AbstractComponentDescriptorInsertRepository implements Com
.setParameter("name", entity.getName()) .setParameter("name", entity.getName())
.setParameter("scope", entity.getScope().name()) .setParameter("scope", entity.getScope().name())
.setParameter("search_text", entity.getSearchText()) .setParameter("search_text", entity.getSearchText())
.setParameter("type", entity.getType().name()); .setParameter("type", entity.getType().name())
.setParameter("clustering_mode", entity.getClusteringMode().name());
} }
private ComponentDescriptorEntity processSaveOrUpdate(ComponentDescriptorEntity entity, String query) { private ComponentDescriptorEntity processSaveOrUpdate(ComponentDescriptorEntity entity, String query) {

4
dao/src/main/java/org/thingsboard/server/dao/sql/component/SqlComponentDescriptorInsertRepository.java

@ -44,10 +44,10 @@ public class SqlComponentDescriptorInsertRepository extends AbstractComponentDes
} }
private static String getInsertOrUpdateStatement(String conflictKeyStatement, String updateKeyStatement) { private static String getInsertOrUpdateStatement(String conflictKeyStatement, String updateKeyStatement) {
return "INSERT INTO component_descriptor (id, created_time, actions, clazz, configuration_descriptor, name, scope, search_text, type) VALUES (:id, :created_time, :actions, :clazz, :configuration_descriptor, :name, :scope, :search_text, :type) ON CONFLICT " + conflictKeyStatement + " DO UPDATE SET " + updateKeyStatement + " returning *"; return "INSERT INTO component_descriptor (id, created_time, actions, clazz, configuration_descriptor, name, scope, search_text, type, clustering_mode) VALUES (:id, :created_time, :actions, :clazz, :configuration_descriptor, :name, :scope, :search_text, :type, :clustering_mode) ON CONFLICT " + conflictKeyStatement + " DO UPDATE SET " + updateKeyStatement + " returning *";
} }
private static String getUpdateStatement(String id) { private static String getUpdateStatement(String id) {
return "actions = :actions, " + id + ",created_time = :created_time, configuration_descriptor = :configuration_descriptor, name = :name, scope = :scope, search_text = :search_text, type = :type"; return "actions = :actions, " + id + ",created_time = :created_time, configuration_descriptor = :configuration_descriptor, name = :name, scope = :scope, search_text = :search_text, type = :type, clustering_mode = :clustering_mode";
} }
} }

6
dao/src/main/java/org/thingsboard/server/dao/sql/notification/NotificationRuleRepository.java

@ -22,7 +22,6 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.dao.model.sql.NotificationRuleEntity; import org.thingsboard.server.dao.model.sql.NotificationRuleEntity;
import org.thingsboard.server.dao.model.sql.NotificationRuleInfoEntity; import org.thingsboard.server.dao.model.sql.NotificationRuleInfoEntity;
@ -42,7 +41,10 @@ public interface NotificationRuleRepository extends JpaRepository<NotificationRu
@Param("searchText") String searchText, @Param("searchText") String searchText,
Pageable pageable); Pageable pageable);
boolean existsByTenantIdAndRecipientsConfigContaining(UUID tenantId, String searchString); @Query("SELECT count(r) > 0 FROM NotificationRuleEntity r WHERE r.tenantId = :tenantId " +
"AND CAST(r.recipientsConfig AS text) LIKE concat('%', :searchString, '%')")
boolean existsByTenantIdAndRecipientsConfigContaining(@Param("tenantId") UUID tenantId,
@Param("searchString") String searchString);
List<NotificationRuleEntity> findAllByTenantIdAndTriggerType(UUID tenantId, NotificationRuleTriggerType triggerType); List<NotificationRuleEntity> findAllByTenantIdAndTriggerType(UUID tenantId, NotificationRuleTriggerType triggerType);

4
dao/src/main/resources/sql/schema-entities.sql

@ -125,7 +125,8 @@ CREATE TABLE IF NOT EXISTS component_descriptor (
name varchar(255), name varchar(255),
scope varchar(255), scope varchar(255),
search_text varchar(255), search_text varchar(255),
type varchar(255) type varchar(255),
clustering_mode varchar(255)
); );
CREATE TABLE IF NOT EXISTS customer ( CREATE TABLE IF NOT EXISTS customer (
@ -187,6 +188,7 @@ CREATE TABLE IF NOT EXISTS rule_node (
type varchar(255), type varchar(255),
name varchar(255), name varchar(255),
debug_mode boolean, debug_mode boolean,
singleton_mode boolean,
search_text varchar(255), search_text varchar(255),
external_id uuid external_id uuid
); );

110
dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo; import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
@ -221,6 +222,115 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest {
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
} }
@Test
public void testFindAlarmV2() throws ExecutionException, InterruptedException {
AssetId parentId = new AssetId(Uuids.timeBased());
AssetId childId = new AssetId(Uuids.timeBased());
EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
Assert.assertTrue(relationService.saveRelationAsync(tenantId, relation).get());
long ts = System.currentTimeMillis();
AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder()
.tenantId(tenantId)
.originator(childId)
.type(TEST_ALARM)
.severity(AlarmSeverity.CRITICAL)
.startTs(ts).build());
AlarmInfo created = result.getAlarm();
// Check child relation
PageData<AlarmInfo> alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder()
.affectedEntityId(childId)
.severityList(Arrays.asList(AlarmSeverity.CRITICAL))
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
// Check parent relation
alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder()
.affectedEntityId(parentId)
.severityList(Arrays.asList(AlarmSeverity.CRITICAL))
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());
created.setPropagate(true);
result = alarmService.updateAlarm(AlarmUpdateRequest.fromAlarm(created));
created = result.getAlarm();
// Check child relation
alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder()
.affectedEntityId(childId)
.severityList(Arrays.asList(AlarmSeverity.CRITICAL))
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
// Check parent relation
alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder()
.affectedEntityId(parentId)
.severityList(Arrays.asList(AlarmSeverity.CRITICAL))
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
alarmService.acknowledgeAlarm(tenantId, created.getId(), System.currentTimeMillis());
created = alarmService.findAlarmInfoById(tenantId, created.getId());
alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder()
.affectedEntityId(childId)
.severityList(Arrays.asList(AlarmSeverity.CRITICAL))
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.ACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
// Check not existing relation
alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder()
.affectedEntityId(childId)
.severityList(Arrays.asList(AlarmSeverity.CRITICAL))
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());
alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null);
created = alarmService.findAlarmInfoById(tenantId, created.getId());
alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder()
.affectedEntityId(childId)
.severityList(Arrays.asList(AlarmSeverity.CRITICAL))
.statusList(Arrays.asList(AlarmSearchStatus.CLEARED, AlarmSearchStatus.ACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
}
@Test @Test
public void testFindAssignedAlarm() throws ExecutionException, InterruptedException { public void testFindAssignedAlarm() throws ExecutionException, InterruptedException {

2
pull_request_template.md

@ -5,7 +5,7 @@ Put your PR description here instead of this sentence.
## General checklist ## General checklist
- [ ] You have reviewed the guidelines [document](https://docs.google.com/document/d/1wqcOafLx5hth8SAg4dqV_LV3un3m5WYR8RdTJ4MbbUM/edit?usp=sharing). - [ ] You have reviewed the guidelines [document](https://docs.google.com/document/d/1wqcOafLx5hth8SAg4dqV_LV3un3m5WYR8RdTJ4MbbUM/edit?usp=sharing).
- [ ] PR name contains fix version. For example, "[3.3.4] Hotfix of some UI component" or "[3.4] New Super Feature". - [ ] [Labels](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/managing-labels#about-labels) that classify your pull request have been added.
- [ ] The [milestone](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/about-milestones) is specified and corresponds to fix version. - [ ] The [milestone](https://docs.github.com/en/issues/using-labels-and-milestones-to-track-work/about-milestones) is specified and corresponds to fix version.
- [ ] Description references specific [issue](https://github.com/thingsboard/thingsboard/issues). - [ ] Description references specific [issue](https://github.com/thingsboard/thingsboard/issues).
- [ ] Description contains human-readable scope of changes. - [ ] Description contains human-readable scope of changes.

2
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/MailService.java

@ -54,4 +54,6 @@ public interface MailService {
void testConnection(TenantId tenantId) throws Exception; void testConnection(TenantId tenantId) throws Exception;
boolean isConfigured(TenantId tenantId);
} }

5
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java

@ -21,6 +21,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
@ -102,6 +103,10 @@ public interface RuleEngineAlarmService {
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId); AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId);
PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds); PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds);

3
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java

@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.api; package org.thingsboard.rule.engine.api;
import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentScope;
import org.thingsboard.server.common.data.plugin.ComponentClusteringMode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainType;
@ -38,6 +39,8 @@ public @interface RuleNode {
Class<? extends NodeConfiguration> configClazz(); Class<? extends NodeConfiguration> configClazz();
ComponentClusteringMode clusteringMode() default ComponentClusteringMode.ENABLED;
boolean inEnabled() default true; boolean inEnabled() default true;
boolean outEnabled() default true; boolean outEnabled() default true;

2
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/slack/SlackService.java

@ -27,8 +27,6 @@ public interface SlackService {
List<SlackConversation> listConversations(TenantId tenantId, String token, SlackConversationType conversationType); List<SlackConversation> listConversations(TenantId tenantId, String token, SlackConversationType conversationType);
SlackConversation findConversation(TenantId tenantId, String token, SlackConversationType conversationType, String namePattern);
String getToken(TenantId tenantId); String getToken(TenantId tenantId);
} }

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java

@ -33,6 +33,7 @@ import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.credentials.BasicCredentials; import org.thingsboard.rule.engine.credentials.BasicCredentials;
import org.thingsboard.rule.engine.credentials.ClientCredentials; import org.thingsboard.rule.engine.credentials.ClientCredentials;
import org.thingsboard.rule.engine.credentials.CredentialsType; import org.thingsboard.rule.engine.credentials.CredentialsType;
import org.thingsboard.server.common.data.plugin.ComponentClusteringMode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -47,6 +48,7 @@ import java.util.concurrent.TimeoutException;
type = ComponentType.EXTERNAL, type = ComponentType.EXTERNAL,
name = "mqtt", name = "mqtt",
configClazz = TbMqttNodeConfiguration.class, configClazz = TbMqttNodeConfiguration.class,
clusteringMode = ComponentClusteringMode.USER_PREFERENCE,
nodeDescription = "Publish messages to the MQTT broker", nodeDescription = "Publish messages to the MQTT broker",
nodeDetails = "Will publish message payload to the MQTT broker with QoS <b>AT_LEAST_ONCE</b>.", nodeDetails = "Will publish message payload to the MQTT broker with QoS <b>AT_LEAST_ONCE</b>.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java

@ -16,9 +16,7 @@
package org.thingsboard.rule.engine.mqtt.azure; package org.thingsboard.rule.engine.mqtt.azure;
import io.netty.handler.codec.mqtt.MqttVersion; import io.netty.handler.codec.mqtt.MqttVersion;
import io.netty.handler.ssl.SslContext;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.common.util.AzureIotHubUtil; import org.thingsboard.common.util.AzureIotHubUtil;
import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttClientConfig;
import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.RuleNode;
@ -26,12 +24,12 @@ import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.credentials.BasicCredentials;
import org.thingsboard.rule.engine.credentials.CertPemCredentials; import org.thingsboard.rule.engine.credentials.CertPemCredentials;
import org.thingsboard.rule.engine.credentials.ClientCredentials; import org.thingsboard.rule.engine.credentials.ClientCredentials;
import org.thingsboard.rule.engine.credentials.CredentialsType; import org.thingsboard.rule.engine.credentials.CredentialsType;
import org.thingsboard.rule.engine.mqtt.TbMqttNode; import org.thingsboard.rule.engine.mqtt.TbMqttNode;
import org.thingsboard.rule.engine.mqtt.TbMqttNodeConfiguration; import org.thingsboard.rule.engine.mqtt.TbMqttNodeConfiguration;
import org.thingsboard.server.common.data.plugin.ComponentClusteringMode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import javax.net.ssl.SSLException; import javax.net.ssl.SSLException;
@ -41,6 +39,7 @@ import javax.net.ssl.SSLException;
type = ComponentType.EXTERNAL, type = ComponentType.EXTERNAL,
name = "azure iot hub", name = "azure iot hub",
configClazz = TbAzureIotHubNodeConfiguration.class, configClazz = TbAzureIotHubNodeConfiguration.class,
clusteringMode = ComponentClusteringMode.SINGLETON,
nodeDescription = "Publish messages to the Azure IoT Hub", nodeDescription = "Publish messages to the Azure IoT Hub",
nodeDetails = "Will publish message payload to the Azure IoT Hub with QoS <b>AT_LEAST_ONCE</b>.", nodeDetails = "Will publish message payload to the Azure IoT Hub with QoS <b>AT_LEAST_ONCE</b>.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},

6
ui-ngx/angular.json

@ -54,6 +54,11 @@
"input": "./node_modules/ace-builds/src-noconflict/", "input": "./node_modules/ace-builds/src-noconflict/",
"output": "/" "output": "/"
}, },
{
"glob": "worker-tbel.js",
"input": "./src/app/shared/models/ace/tbel/",
"output": "/"
},
{ {
"glob": "**/*", "glob": "**/*",
"input": "node_modules/tinymce", "input": "node_modules/tinymce",
@ -85,6 +90,7 @@
"node_modules/jstree-bootstrap-theme/dist/themes/proton/style.min.css", "node_modules/jstree-bootstrap-theme/dist/themes/proton/style.min.css",
"node_modules/leaflet/dist/leaflet.css", "node_modules/leaflet/dist/leaflet.css",
"src/app/modules/home/components/widget/lib/maps/markers.scss", "src/app/modules/home/components/widget/lib/maps/markers.scss",
"src/app/modules/home/components/widget/lib/home-page/home-page.scss",
"node_modules/leaflet.markercluster/dist/MarkerCluster.css", "node_modules/leaflet.markercluster/dist/MarkerCluster.css",
"node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css", "node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css",
"node_modules/@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css", "node_modules/@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css",

32
ui-ngx/src/app/core/api/alias-controller.ts

@ -24,6 +24,8 @@ import { AliasFilterType, EntityAliases, SingleEntityFilter } from '@shared/mode
import { EntityInfo } from '@shared/models/entity.models'; import { EntityInfo } from '@shared/models/entity.models';
import { map, mergeMap } from 'rxjs/operators'; import { map, mergeMap } from 'rxjs/operators';
import { import {
AlarmFilter,
AlarmFilterConfig,
createDefaultEntityDataPageLink, createDefaultEntityDataPageLink,
Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink, Filter, FilterInfo, filterInfoToKeyFilters, Filters, KeyFilter, singleEntityDataPageLink,
updateDatasourceFromEntityInfo updateDatasourceFromEntityInfo
@ -244,10 +246,14 @@ export class AliasController implements IAliasController {
private resolveDatasource(datasource: Datasource, forceFilter = false): Observable<Datasource> { private resolveDatasource(datasource: Datasource, forceFilter = false): Observable<Datasource> {
const newDatasource = deepClone(datasource); const newDatasource = deepClone(datasource);
if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount) { if (newDatasource.type === DatasourceType.entity || newDatasource.type === DatasourceType.entityCount
|| newDatasource.type === DatasourceType.alarmCount) {
if (newDatasource.filterId) { if (newDatasource.filterId) {
newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId); newDatasource.keyFilters = this.getKeyFilters(newDatasource.filterId);
} }
if (newDatasource.type === DatasourceType.alarmCount) {
newDatasource.alarmFilter = this.entityService.resolveAlarmFilter(newDatasource.alarmFilterConfig, false);
}
if (newDatasource.entityAliasId) { if (newDatasource.entityAliasId) {
return this.getAliasInfo(newDatasource.entityAliasId).pipe( return this.getAliasInfo(newDatasource.entityAliasId).pipe(
mergeMap((aliasInfo) => { mergeMap((aliasInfo) => {
@ -336,22 +342,30 @@ export class AliasController implements IAliasController {
map((result) => { map((result) => {
let functionIndex = 0; let functionIndex = 0;
let entityCountIndex = 0; let entityCountIndex = 0;
let alarmCountIndex = 0;
result.forEach((datasource) => { result.forEach((datasource) => {
if (datasource.type === DatasourceType.function || datasource.type === DatasourceType.entityCount) { if (datasource.type === DatasourceType.function || datasource.type === DatasourceType.entityCount ||
datasource.type === DatasourceType.alarmCount) {
let name: string; let name: string;
if (datasource.name && datasource.name.length) { if (datasource.name && datasource.name.length) {
name = datasource.name; name = datasource.name;
} else { } else {
name = this.translate.instant(datasourceTypeTranslationMap.get(datasource.type));
if (datasource.type === DatasourceType.function) { if (datasource.type === DatasourceType.function) {
functionIndex++; functionIndex++;
} else { if (functionIndex > 1) {
name += ' ' + functionIndex;
}
} else if (datasource.type === DatasourceType.entityCount) {
entityCountIndex++; entityCountIndex++;
} if (entityCountIndex > 1) {
name = this.translate.instant(datasourceTypeTranslationMap.get(datasource.type)); name += ' ' + entityCountIndex;
if (datasource.type === DatasourceType.function && functionIndex > 1) { }
name += ' ' + functionIndex; } else {
} else if (datasource.type === DatasourceType.entityCount && entityCountIndex > 1) { alarmCountIndex++;
name += ' ' + entityCountIndex; if (alarmCountIndex > 1) {
name += ' ' + alarmCountIndex;
}
} }
} }
datasource.name = name; datasource.name = name;

91
ui-ngx/src/app/core/api/entity-data-subscription.ts

@ -23,6 +23,7 @@ import {
SubscriptionTimewindow SubscriptionTimewindow
} from '@shared/models/time/time.models'; } from '@shared/models/time/time.models';
import { import {
AlarmFilter,
ComparisonTsValue, ComparisonTsValue,
EntityData, EntityData,
EntityDataPageLink, EntityDataPageLink,
@ -36,6 +37,7 @@ import {
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { import {
AggKey, AggKey,
AlarmCountCmd,
DataKeyType, DataKeyType,
EntityCountCmd, EntityCountCmd,
EntityDataCmd, EntityDataCmd,
@ -84,6 +86,7 @@ export interface EntityDataSubscriptionOptions {
dataKeys: Array<SubscriptionDataKey>; dataKeys: Array<SubscriptionDataKey>;
type: widgetType; type: widgetType;
entityFilter?: EntityFilter; entityFilter?: EntityFilter;
alarmFilter?: AlarmFilter;
isPaginatedDataSubscription?: boolean; isPaginatedDataSubscription?: boolean;
ignoreDataUpdateOnIntervalTick?: boolean; ignoreDataUpdateOnIntervalTick?: boolean;
pageLink?: EntityDataPageLink; pageLink?: EntityDataPageLink;
@ -111,6 +114,7 @@ export class EntityDataSubscription {
private dataCommand: EntityDataCmd; private dataCommand: EntityDataCmd;
private subsCommand: EntityDataCmd; private subsCommand: EntityDataCmd;
private countCommand: EntityCountCmd; private countCommand: EntityCountCmd;
private alarmCountCommand: AlarmCountCmd;
private attrFields: Array<EntityKey>; private attrFields: Array<EntityKey>;
private tsFields: Array<EntityKey>; private tsFields: Array<EntityKey>;
@ -197,6 +201,7 @@ export class EntityDataSubscription {
} }
let key: string; let key: string;
if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount || if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount ||
this.datasourceType === DatasourceType.alarmCount ||
this.entityDataSubscriptionOptions.type === widgetType.timeseries) { this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
if (this.datasourceType === DatasourceType.function) { if (this.datasourceType === DatasourceType.function) {
key = `${dataKey.name}_${dataKey.index}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`; key = `${dataKey.name}_${dataKey.index}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`;
@ -228,7 +233,8 @@ export class EntityDataSubscription {
clearTimeout(this.latestTimer); clearTimeout(this.latestTimer);
this.latestTimer = null; this.latestTimer = null;
} }
if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount) { if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount
|| this.datasourceType === DatasourceType.alarmCount) {
if (this.subscriber) { if (this.subscriber) {
this.subscriber.unsubscribe(); this.subscriber.unsubscribe();
this.subscriber = null; this.subscriber = null;
@ -508,6 +514,84 @@ export class EntityDataSubscription {
} }
); );
this.subscriber.subscribe(); this.subscriber.subscribe();
} else if (this.datasourceType === DatasourceType.alarmCount) {
this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset;
this.subscriber = new TelemetrySubscriber(this.telemetryService);
this.subscriber.setTsOffset(this.latestTsOffset);
this.alarmCountCommand = new AlarmCountCmd();
let keyFilters = this.entityDataSubscriptionOptions.keyFilters;
if (this.entityDataSubscriptionOptions.additionalKeyFilters) {
if (keyFilters) {
keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters);
} else {
keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters;
}
}
this.alarmCountCommand.query = {
entityFilter: this.entityDataSubscriptionOptions.entityFilter,
keyFilters
};
if (this.entityDataSubscriptionOptions.alarmFilter) {
this.alarmCountCommand.query = {...this.alarmCountCommand.query, ...this.entityDataSubscriptionOptions.alarmFilter};
}
this.subscriber.subscriptionCommands.push(this.alarmCountCommand);
const entityId: EntityId = {
id: NULL_UUID,
entityType: null
};
const countKey = this.dataKeysList[0];
let dataReceived = false;
this.subscriber.alarmCount$.subscribe(
(alarmCountUpdate) => {
if (!dataReceived) {
const entityData: EntityData = {
entityId,
latest: {
[EntityKeyType.ENTITY_FIELD]: {
name: {
ts: Date.now() + this.latestTsOffset,
value: DatasourceType.alarmCount
}
},
[EntityKeyType.COUNT]: {
[countKey.name]: {
ts: Date.now() + this.latestTsOffset,
value: alarmCountUpdate.count + ''
}
}
},
timeseries: {}
};
const pageData: PageData<EntityData> = {
data: [entityData],
hasNext: false,
totalElements: 1,
totalPages: 1
};
this.onPageData(pageData);
dataReceived = true;
} else {
const update: EntityData[] = [{
entityId,
latest: {
[EntityKeyType.COUNT]: {
[countKey.name]: {
ts: Date.now() + this.latestTsOffset,
value: alarmCountUpdate.count + ''
}
}
},
timeseries: {}
}];
this.onDataUpdate(update);
}
}
);
this.subscriber.subscribe();
} }
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
return of(null); return of(null);
@ -542,7 +626,7 @@ export class EntityDataSubscription {
this.subscriber.subscriptionCommands = [this.subsCommand]; this.subscriber.subscriptionCommands = [this.subsCommand];
this.subscriber.update(); this.subscriber.update();
} }
} else if (this.datasourceType === DatasourceType.entityCount) { } else if (this.datasourceType === DatasourceType.entityCount || this.datasourceType === DatasourceType.alarmCount) {
if (this.subscriber.setTsOffset(this.latestTsOffset)) { if (this.subscriber.setTsOffset(this.latestTsOffset)) {
if (this.listener.forceReInit) { if (this.listener.forceReInit) {
this.listener.forceReInit(); this.listener.forceReInit();
@ -729,7 +813,8 @@ export class EntityDataSubscription {
this.datasourceData[dataIndex] = {}; this.datasourceData[dataIndex] = {};
for (const key of Object.keys(this.dataKeys)) { for (const key of Object.keys(this.dataKeys)) {
const dataKey = this.dataKeys[key]; const dataKey = this.dataKeys[key];
if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount || if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount
|| this.datasourceType === DatasourceType.alarmCount ||
this.entityDataSubscriptionOptions.type === widgetType.timeseries) { this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
const dataKeysList = dataKey as Array<SubscriptionDataKey>; const dataKeysList = dataKey as Array<SubscriptionDataKey>;
for (let index = 0; index < dataKeysList.length; index++) { for (let index = 0; index < dataKeysList.length; index++) {

4
ui-ngx/src/app/core/api/entity-data.service.ts

@ -175,8 +175,10 @@ export class EntityDataService {
type: subscriptionType type: subscriptionType
}; };
if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity || if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity ||
entityDataSubscriptionOptions.datasourceType === DatasourceType.entityCount) { entityDataSubscriptionOptions.datasourceType === DatasourceType.entityCount ||
entityDataSubscriptionOptions.datasourceType === DatasourceType.alarmCount) {
entityDataSubscriptionOptions.entityFilter = datasource.entityFilter; entityDataSubscriptionOptions.entityFilter = datasource.entityFilter;
entityDataSubscriptionOptions.alarmFilter = datasource.alarmFilter;
entityDataSubscriptionOptions.keyFilters = keyFilters; entityDataSubscriptionOptions.keyFilters = keyFilters;
entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters; entityDataSubscriptionOptions.additionalKeyFilters = additionalKeyFilters;
if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) { if (entityDataSubscriptionOptions.datasourceType === DatasourceType.entity) {

14
ui-ngx/src/app/core/http/alarm.service.ts

@ -23,7 +23,7 @@ import { EntityId } from '@shared/models/id/entity-id';
import { import {
Alarm, Alarm,
AlarmInfo, AlarmInfo,
AlarmQuery, AlarmQuery, AlarmQueryV2,
AlarmSearchStatus, AlarmSearchStatus,
AlarmSeverity, AlarmSeverity,
AlarmStatus AlarmStatus
@ -78,12 +78,24 @@ export class AlarmService {
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public getAlarmsV2(query: AlarmQueryV2,
config?: RequestConfig): Observable<PageData<AlarmInfo>> {
return this.http.get<PageData<AlarmInfo>>(`/api/v2/alarm${query.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public getAllAlarms(query: AlarmQuery, public getAllAlarms(query: AlarmQuery,
config?: RequestConfig): Observable<PageData<AlarmInfo>> { config?: RequestConfig): Observable<PageData<AlarmInfo>> {
return this.http.get<PageData<AlarmInfo>>(`/api/alarms${query.toQuery()}`, return this.http.get<PageData<AlarmInfo>>(`/api/alarms${query.toQuery()}`,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public getAllAlarmsV2(query: AlarmQueryV2,
config?: RequestConfig): Observable<PageData<AlarmInfo>> {
return this.http.get<PageData<AlarmInfo>>(`/api/v2/alarms${query.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public getHighestAlarmSeverity(entityId: EntityId, alarmSearchStatus: AlarmSearchStatus, alarmStatus: AlarmStatus, public getHighestAlarmSeverity(entityId: EntityId, alarmSearchStatus: AlarmSearchStatus, alarmStatus: AlarmStatus,
config?: RequestConfig): Observable<AlarmSeverity> { config?: RequestConfig): Observable<AlarmSeverity> {
let url = `/api/alarm/highestSeverity/${entityId.entityType}/${entityId.id}`; let url = `/api/alarm/highestSeverity/${entityId.entityType}/${entityId.id}`;

26
ui-ngx/src/app/core/http/entity.service.ts

@ -65,7 +65,7 @@ import { Device, DeviceCredentialsType } from '@shared/models/device.models';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
import { import {
AlarmData, AlarmData,
AlarmDataQuery, AlarmDataQuery, AlarmFilter, AlarmFilterConfig,
createDefaultEntityDataPageLink, createDefaultEntityDataPageLink,
EntityData, EntityData,
EntityDataQuery, EntityDataQuery,
@ -91,6 +91,7 @@ import { AssetProfileService } from '@core/http/asset-profile.service';
import { NotificationService } from '@core/http/notification.service'; import { NotificationService } from '@core/http/notification.service';
import { TenantProfileService } from '@core/http/tenant-profile.service'; import { TenantProfileService } from '@core/http/tenant-profile.service';
import { NotificationType } from '@shared/models/notification.models'; import { NotificationType } from '@shared/models/notification.models';
import { UserId } from '@shared/models/id/user-id';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -1010,6 +1011,24 @@ export class EntityService {
); );
} }
public resolveAlarmFilter(alarmFilterConfig?: AlarmFilterConfig, searchPropagatedByDefault = true): AlarmFilter {
const alarmFilter: AlarmFilter = {};
if (alarmFilterConfig) {
alarmFilter.typeList = alarmFilterConfig.typeList;
alarmFilter.severityList = alarmFilterConfig.severityList;
alarmFilter.statusList = alarmFilterConfig.statusList;
alarmFilter.searchPropagatedAlarms = isDefined(alarmFilterConfig.searchPropagatedAlarms) ?
alarmFilterConfig.searchPropagatedAlarms : searchPropagatedByDefault;
if (alarmFilterConfig.assignedToCurrentUser) {
const authUser = getCurrentAuthUser(this.store);
alarmFilter.assigneeId = new UserId(authUser.userId);
} else {
alarmFilter.assigneeId = alarmFilterConfig.assigneeId;
}
}
return alarmFilter;
}
public saveEntityParameters(entityType: EntityType, entityData: ImportEntityData, update: boolean, public saveEntityParameters(entityType: EntityType, entityData: ImportEntityData, update: boolean,
config?: RequestConfig): Observable<ImportEntitiesResultInfo> { config?: RequestConfig): Observable<ImportEntitiesResultInfo> {
const saveEntityObservable: Observable<BaseData<EntityId>> = this.getSaveEntityObservable(entityType, entityData, config); const saveEntityObservable: Observable<BaseData<EntityId>> = this.getSaveEntityObservable(entityType, entityData, config);
@ -1321,7 +1340,8 @@ export class EntityService {
dataKeys: [] dataKeys: []
}; };
this.prepareEntityFilterFromSubscriptionInfo(datasource, subscriptionInfo); this.prepareEntityFilterFromSubscriptionInfo(datasource, subscriptionInfo);
} else if (subscriptionInfo.type === DatasourceType.function || subscriptionInfo.type === DatasourceType.entityCount) { } else if (subscriptionInfo.type === DatasourceType.function || subscriptionInfo.type === DatasourceType.entityCount ||
subscriptionInfo.type === DatasourceType.alarmCount) {
datasource = { datasource = {
type: subscriptionInfo.type, type: subscriptionInfo.type,
name: subscriptionInfo.name || subscriptionInfo.type, name: subscriptionInfo.name || subscriptionInfo.type,
@ -1341,7 +1361,7 @@ export class EntityService {
if (subscriptionInfo.alarmFields) { if (subscriptionInfo.alarmFields) {
this.createDatasourceKeys(subscriptionInfo.alarmFields, DataKeyType.alarm, datasource); this.createDatasourceKeys(subscriptionInfo.alarmFields, DataKeyType.alarm, datasource);
} }
if (subscriptionInfo.type === DatasourceType.entityCount) { if (subscriptionInfo.type === DatasourceType.entityCount || subscriptionInfo.type === DatasourceType.alarmCount) {
const dataKey = this.utils.createKey({ name: 'count'}, DataKeyType.count); const dataKey = this.utils.createKey({ name: 'count'}, DataKeyType.count);
datasource.dataKeys.push(dataKey); datasource.dataKeys.push(dataKey);
} }

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

@ -22,6 +22,7 @@ import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { import {
Notification, Notification,
NotificationDeliveryMethod,
NotificationRequest, NotificationRequest,
NotificationRequestInfo, NotificationRequestInfo,
NotificationRequestPreview, NotificationRequestPreview,
@ -71,6 +72,10 @@ export class NotificationService {
return this.http.get<NotificationRequest>(`/api/notification/request/${id}`, defaultHttpOptionsFromConfig(config)); return this.http.get<NotificationRequest>(`/api/notification/request/${id}`, defaultHttpOptionsFromConfig(config));
} }
public getAvailableDeliveryMethods(config?: RequestConfig): Observable<Array<NotificationDeliveryMethod>> {
return this.http.get<Array<NotificationDeliveryMethod>>(`/api/notification/deliveryMethods`, defaultHttpOptionsFromConfig(config));
}
public deleteNotificationRequest(id: string, config?: RequestConfig): Observable<void> { public deleteNotificationRequest(id: string, config?: RequestConfig): Observable<void> {
return this.http.delete<void>(`/api/notification/request/${id}`, defaultHttpOptionsFromConfig(config)); return this.http.delete<void>(`/api/notification/request/${id}`, defaultHttpOptionsFromConfig(config));
} }

1
ui-ngx/src/app/core/http/public-api.ts

@ -42,3 +42,4 @@ export * from './ui-settings.service';
export * from './user.service'; export * from './user.service';
export * from './user-settings.service'; export * from './user-settings.service';
export * from './widget.service'; export * from './widget.service';
export * from './usage-info.service';

37
ui-ngx/src/app/core/http/usage-info.service.ts

@ -0,0 +1,37 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { UsageInfo } from '@shared/models/usage.models';
@Injectable({
providedIn: 'root'
})
export class UsageInfoService {
constructor(
private http: HttpClient
) {}
public getUsageInfo(config?: RequestConfig): Observable<UsageInfo> {
return this.http.get<UsageInfo>('/api/usage',
defaultHttpOptionsFromConfig(config));
}
}

23
ui-ngx/src/app/core/http/user-settings.service.ts

@ -19,7 +19,7 @@ import { Observable } from 'rxjs';
import { import {
DocumentationLink, DocumentationLinks, DocumentationLink, DocumentationLinks,
GettingStarted, GettingStarted,
initialUserSettings, initialUserSettings, QuickLinks, UserDashboardAction, UserDashboardsInfo,
UserSettings, UserSettings,
UserSettingsType UserSettingsType
} from '@shared/models/user-settings.models'; } from '@shared/models/user-settings.models';
@ -67,6 +67,16 @@ export class UserSettingsService {
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public getQuickLinks(config?: RequestConfig): Observable<QuickLinks> {
return this.http.get<QuickLinks>(`/api/user/settings/${UserSettingsType.QUICK_LINKS}`,
defaultHttpOptionsFromConfig(config));
}
public updateQuickLinks(quickLinks: QuickLinks, config?: RequestConfig): Observable<void> {
return this.http.put<void>(`/api/user/settings/${UserSettingsType.QUICK_LINKS}`, quickLinks,
defaultHttpOptionsFromConfig(config));
}
public getGettingStarted(config?: RequestConfig): Observable<GettingStarted> { public getGettingStarted(config?: RequestConfig): Observable<GettingStarted> {
return this.http.get<GettingStarted>(`/api/user/settings/${UserSettingsType.GETTING_STARTED}`, return this.http.get<GettingStarted>(`/api/user/settings/${UserSettingsType.GETTING_STARTED}`,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
@ -77,4 +87,15 @@ export class UserSettingsService {
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public getUserDashboardsInfo(config?: RequestConfig): Observable<UserDashboardsInfo> {
return this.http.get<UserDashboardsInfo>('/api/user/dashboards',
defaultHttpOptionsFromConfig(config));
}
public reportUserDashboardAction(dashboardId: string, action: UserDashboardAction,
config?: RequestConfig): Observable<UserDashboardsInfo> {
return this.http.get<UserDashboardsInfo>(`/api/user/dashboards/${dashboardId}/${action}`,
defaultHttpOptionsFromConfig(config));
}
} }

69
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -30,11 +30,12 @@ import {
WidgetLayout WidgetLayout
} from '@shared/models/dashboard.models'; } from '@shared/models/dashboard.models';
import { isDefined, isString, isUndefined } from '@core/utils'; import { isDefined, isString, isUndefined } from '@core/utils';
import { Datasource, DatasourceType, Widget, widgetType } from '@app/shared/models/widget.models'; import { Datasource, DatasourceType, Widget, WidgetConfig, widgetType } from '@app/shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models'; import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models';
import { EntityId } from '@app/shared/models/id/entity-id'; import { EntityId } from '@app/shared/models/id/entity-id';
import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'; import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models';
import { AlarmSearchStatus } from '@shared/models/alarm.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -201,13 +202,22 @@ export class DashboardUtilsService {
} }
public validateAndUpdateWidget(widget: Widget): Widget { public validateAndUpdateWidget(widget: Widget): Widget {
if (!widget.config) { widget.config = this.validateAndUpdateWidgetConfig(widget.config, widget.type);
widget.config = {}; // Temp workaround
if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') {
widget.typeAlias = 'basic_timeseries';
}
return widget;
}
public validateAndUpdateWidgetConfig(widgetConfig: WidgetConfig | undefined, type: widgetType): WidgetConfig {
if (!widgetConfig) {
widgetConfig = {};
} }
if (!widget.config.datasources) { if (!widgetConfig.datasources) {
widget.config.datasources = []; widgetConfig.datasources = [];
} }
widget.config.datasources.forEach((datasource) => { widgetConfig.datasources.forEach((datasource) => {
if (datasource.type === 'device') { if (datasource.type === 'device') {
datasource.type = DatasourceType.entity; datasource.type = DatasourceType.entity;
} }
@ -216,14 +226,47 @@ export class DashboardUtilsService {
delete datasource.deviceAliasId; delete datasource.deviceAliasId;
} }
}); });
if (widget.type === widgetType.latest) { if (type === widgetType.latest) {
widget.config.timewindow = initModelFromDefaultTimewindow(widget.config.timewindow, true, this.timeService); widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, this.timeService);
} }
// Temp workaround if (type === widgetType.alarm) {
if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') { if (!widgetConfig.alarmFilterConfig) {
widget.typeAlias = 'basic_timeseries'; widgetConfig.alarmFilterConfig = {};
const alarmFilterConfig = widgetConfig.alarmFilterConfig;
if (isDefined(widgetConfig.alarmStatusList) && widgetConfig.alarmStatusList.length) {
alarmFilterConfig.statusList = widgetConfig.alarmStatusList;
} else if (isDefined(widgetConfig.alarmSearchStatus) && widgetConfig.alarmSearchStatus !== AlarmSearchStatus.ANY) {
alarmFilterConfig.statusList = [widgetConfig.alarmSearchStatus];
} else {
alarmFilterConfig.statusList = [];
}
if (isDefined(widgetConfig.alarmStatusList)) {
delete widgetConfig.alarmStatusList;
}
if (isDefined(widgetConfig.alarmSearchStatus)) {
delete widgetConfig.alarmSearchStatus;
}
if (isDefined(widgetConfig.alarmSeverityList)) {
alarmFilterConfig.severityList = widgetConfig.alarmSeverityList;
delete widgetConfig.alarmSeverityList;
} else {
alarmFilterConfig.severityList = [];
}
if (isDefined(widgetConfig.alarmTypeList)) {
alarmFilterConfig.typeList = widgetConfig.alarmTypeList;
delete widgetConfig.alarmTypeList;
} else {
alarmFilterConfig.typeList = [];
}
if (isDefined(widgetConfig.searchPropagatedAlarms)) {
alarmFilterConfig.searchPropagatedAlarms = widgetConfig.searchPropagatedAlarms;
delete widgetConfig.searchPropagatedAlarms;
} else {
alarmFilterConfig.searchPropagatedAlarms = true;
}
}
} }
return widget; return widgetConfig;
} }
public createDefaultLayoutData(): DashboardLayout { public createDefaultLayoutData(): DashboardLayout {

8
ui-ngx/src/app/core/services/item-buffer.service.ts

@ -101,7 +101,9 @@ export class ItemBufferService {
if (datasources) { if (datasources) {
for (let i = 0; i < datasources.length; i++) { for (let i = 0; i < datasources.length; i++) {
const datasource = datasources[i]; const datasource = datasources[i];
if ((datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount) && datasource.entityAliasId) { if ((datasource.type === DatasourceType.entity ||
datasource.type === DatasourceType.entityCount ||
datasource.type === DatasourceType.alarmCount) && datasource.entityAliasId) {
entityAlias = dashboard.configuration.entityAliases[datasource.entityAliasId]; entityAlias = dashboard.configuration.entityAliases[datasource.entityAliasId];
if (entityAlias) { if (entityAlias) {
aliasesInfo.datasourceAliases[i] = this.prepareAliasInfo(entityAlias); aliasesInfo.datasourceAliases[i] = this.prepareAliasInfo(entityAlias);
@ -127,7 +129,9 @@ export class ItemBufferService {
if (datasources) { if (datasources) {
for (let i = 0; i < datasources.length; i++) { for (let i = 0; i < datasources.length; i++) {
const datasource = datasources[i]; const datasource = datasources[i];
if ((datasource.type === DatasourceType.entity || datasource.type === DatasourceType.entityCount) && datasource.filterId) { if ((datasource.type === DatasourceType.entity ||
datasource.type === DatasourceType.entityCount ||
datasource.type === DatasourceType.alarmCount) && datasource.filterId) {
filter = dashboard.configuration.filters[datasource.filterId]; filter = dashboard.configuration.filters[datasource.filterId];
if (filter) { if (filter) {
filtersInfo.datasourceFilters[i] = this.prepareFilterInfo(filter); filtersInfo.datasourceFilters[i] = this.prepareFilterInfo(filter);

1
ui-ngx/src/app/core/services/menu.models.ts

@ -20,6 +20,7 @@ export declare type MenuSectionType = 'link' | 'toggle';
export interface MenuSection extends HasUUID{ export interface MenuSection extends HasUUID{
name: string; name: string;
fullName?: string;
type: MenuSectionType; type: MenuSectionType;
path: string; path: string;
icon: string; icon: string;

193
ui-ngx/src/app/core/services/menu.service.ts

@ -18,11 +18,10 @@ import { Injectable } from '@angular/core';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { AppState } from '../core.state'; import { AppState } from '../core.state';
import { getCurrentOpenedMenuSections, selectAuth, selectIsAuthenticated } from '../auth/auth.selectors'; import { getCurrentOpenedMenuSections, selectAuth, selectIsAuthenticated } from '../auth/auth.selectors';
import { filter, take } from 'rxjs/operators'; import { filter, map, take } from 'rxjs/operators';
import { HomeSection, MenuSection } from '@core/services/menu.models'; import { HomeSection, MenuSection } from '@core/services/menu.models';
import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { guid } from '@core/utils';
import { AuthState } from '@core/auth/auth.models'; import { AuthState } from '@core/auth/auth.models';
import { NavigationEnd, Router } from '@angular/router'; import { NavigationEnd, Router } from '@angular/router';
@ -34,6 +33,9 @@ export class MenuService {
currentMenuSections: Array<MenuSection>; currentMenuSections: Array<MenuSection>;
menuSections$: Subject<Array<MenuSection>> = new BehaviorSubject<Array<MenuSection>>([]); menuSections$: Subject<Array<MenuSection>> = new BehaviorSubject<Array<MenuSection>>([]);
homeSections$: Subject<Array<HomeSection>> = new BehaviorSubject<Array<HomeSection>>([]); homeSections$: Subject<Array<HomeSection>> = new BehaviorSubject<Array<HomeSection>>([]);
availableMenuLinks$ = this.menuSections$.pipe(
map((items) => this.allMenuLinks(items))
);
constructor(private store: Store<AppState>, constructor(private store: Store<AppState>,
private router: Router) { private router: Router) {
@ -91,21 +93,21 @@ export class MenuService {
const sections: Array<MenuSection> = []; const sections: Array<MenuSection> = [];
sections.push( sections.push(
{ {
id: guid(), id: 'home',
name: 'home.home', name: 'home.home',
type: 'link', type: 'link',
path: '/home', path: '/home',
icon: 'home' icon: 'home'
}, },
{ {
id: guid(), id: 'tenants',
name: 'tenant.tenants', name: 'tenant.tenants',
type: 'link', type: 'link',
path: '/tenants', path: '/tenants',
icon: 'supervisor_account' icon: 'supervisor_account'
}, },
{ {
id: guid(), id: 'tenant_profiles',
name: 'tenant-profile.tenant-profiles', name: 'tenant-profile.tenant-profiles',
type: 'link', type: 'link',
path: '/tenantProfiles', path: '/tenantProfiles',
@ -113,21 +115,21 @@ export class MenuService {
isMdiIcon: true isMdiIcon: true
}, },
{ {
id: guid(), id: 'resources',
name: 'admin.resources', name: 'admin.resources',
type: 'toggle', type: 'toggle',
path: '/resources', path: '/resources',
icon: 'folder', icon: 'folder',
pages: [ pages: [
{ {
id: guid(), id: 'widget_library',
name: 'widget.widget-library', name: 'widget.widget-library',
type: 'link', type: 'link',
path: '/resources/widgets-bundles', path: '/resources/widgets-bundles',
icon: 'now_widgets' icon: 'now_widgets'
}, },
{ {
id: guid(), id: 'resources_library',
name: 'resource.resources-library', name: 'resource.resources-library',
type: 'link', type: 'link',
path: '/resources/resources-library', path: '/resources/resources-library',
@ -137,7 +139,7 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'notifications_center',
name: 'notification.notification-center', name: 'notification.notification-center',
type: 'link', type: 'link',
path: '/notification', path: '/notification',
@ -145,37 +147,42 @@ export class MenuService {
isMdiIcon: true, isMdiIcon: true,
pages: [ pages: [
{ {
id: guid(), id: 'notification_inbox',
name: 'notification.inbox', name: 'notification.inbox',
fullName: 'notification.notification-inbox',
type: 'link', type: 'link',
path: '/notification/inbox', path: '/notification/inbox',
icon: 'inbox' icon: 'inbox'
}, },
{ {
id: guid(), id: 'notification_sent',
name: 'notification.sent', name: 'notification.sent',
fullName: 'notification.notification-sent',
type: 'link', type: 'link',
path: '/notification/sent', path: '/notification/sent',
icon: 'outbox' icon: 'outbox'
}, },
{ {
id: guid(), id: 'notification_recipients',
name: 'notification.recipients', name: 'notification.recipients',
fullName: 'notification.notification-recipients',
type: 'link', type: 'link',
path: '/notification/recipients', path: '/notification/recipients',
icon: 'contacts' icon: 'contacts'
}, },
{ {
id: guid(), id: 'notification_templates',
name: 'notification.templates', name: 'notification.templates',
fullName: 'notification.notification-templates',
type: 'link', type: 'link',
path: '/notification/templates', path: '/notification/templates',
icon: 'mdi:message-draw', icon: 'mdi:message-draw',
isMdiIcon: true isMdiIcon: true
}, },
{ {
id: guid(), id: 'notification_rules',
name: 'notification.rules', name: 'notification.rules',
fullName: 'notification.notification-rules',
type: 'link', type: 'link',
path: '/notification/rules', path: '/notification/rules',
icon: 'mdi:message-cog', icon: 'mdi:message-cog',
@ -184,36 +191,38 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'settings',
name: 'admin.settings', name: 'admin.settings',
type: 'link', type: 'link',
path: '/settings', path: '/settings',
icon: 'settings', icon: 'settings',
pages: [ pages: [
{ {
id: guid(), id: 'general',
name: 'admin.general', name: 'admin.general',
fullName: 'admin.general-settings',
type: 'link', type: 'link',
path: '/settings/general', path: '/settings/general',
icon: 'settings_applications' icon: 'settings_applications'
}, },
{ {
id: guid(), id: 'mail_server',
name: 'admin.outgoing-mail', name: 'admin.outgoing-mail',
type: 'link', type: 'link',
path: '/settings/outgoing-mail', path: '/settings/outgoing-mail',
icon: 'mail' icon: 'mail'
}, },
{ {
id: guid(), id: 'notification_settings',
name: 'admin.notifications', name: 'admin.notifications',
fullName: 'admin.notifications-settings',
type: 'link', type: 'link',
path: '/settings/notifications', path: '/settings/notifications',
icon: 'mdi:message-badge', icon: 'mdi:message-badge',
isMdiIcon: true isMdiIcon: true
}, },
{ {
id: guid(), id: 'queues',
name: 'admin.queues', name: 'admin.queues',
type: 'link', type: 'link',
path: '/settings/queues', path: '/settings/queues',
@ -222,21 +231,22 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'security_settings',
name: 'security.security', name: 'security.security',
type: 'toggle', type: 'toggle',
path: '/security-settings', path: '/security-settings',
icon: 'security', icon: 'security',
pages: [ pages: [
{ {
id: guid(), id: 'security_settings_general',
name: 'admin.general', name: 'admin.general',
fullName: 'security.general-settings',
type: 'link', type: 'link',
path: '/security-settings/general', path: '/security-settings/general',
icon: 'settings_applications' icon: 'settings_applications'
}, },
{ {
id: guid(), id: '2fa',
name: 'admin.2fa.2fa', name: 'admin.2fa.2fa',
type: 'link', type: 'link',
path: '/security-settings/2fa', path: '/security-settings/2fa',
@ -244,7 +254,7 @@ export class MenuService {
isMdiIcon: true isMdiIcon: true
}, },
{ {
id: guid(), id: 'oauth2',
name: 'admin.oauth2.oauth2', name: 'admin.oauth2.oauth2',
type: 'link', type: 'link',
path: '/security-settings/oauth2', path: '/security-settings/oauth2',
@ -340,49 +350,49 @@ export class MenuService {
const sections: Array<MenuSection> = []; const sections: Array<MenuSection> = [];
sections.push( sections.push(
{ {
id: guid(), id: 'home',
name: 'home.home', name: 'home.home',
type: 'link', type: 'link',
path: '/home', path: '/home',
icon: 'home' icon: 'home'
}, },
{ {
id: guid(), id: 'alarms',
name: 'alarm.alarms', name: 'alarm.alarms',
type: 'link', type: 'link',
path: '/alarms', path: '/alarms',
icon: 'notifications' icon: 'notifications'
}, },
{ {
id: guid(), id: 'dashboards',
name: 'dashboard.dashboards', name: 'dashboard.dashboards',
type: 'link', type: 'link',
path: '/dashboards', path: '/dashboards',
icon: 'dashboards' icon: 'dashboards'
}, },
{ {
id: guid(), id: 'entities',
name: 'entity.entities', name: 'entity.entities',
type: 'toggle', type: 'toggle',
path: '/entities', path: '/entities',
icon: 'category', icon: 'category',
pages: [ pages: [
{ {
id: guid(), id: 'devices',
name: 'device.devices', name: 'device.devices',
type: 'link', type: 'link',
path: '/entities/devices', path: '/entities/devices',
icon: 'devices_other' icon: 'devices_other'
}, },
{ {
id: guid(), id: 'assets',
name: 'asset.assets', name: 'asset.assets',
type: 'link', type: 'link',
path: '/entities/assets', path: '/entities/assets',
icon: 'domain' icon: 'domain'
}, },
{ {
id: guid(), id: 'entity_views',
name: 'entity-view.entity-views', name: 'entity-view.entity-views',
type: 'link', type: 'link',
path: '/entities/entityViews', path: '/entities/entityViews',
@ -391,14 +401,14 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'profiles',
name: 'profiles.profiles', name: 'profiles.profiles',
type: 'toggle', type: 'toggle',
path: '/profiles', path: '/profiles',
icon: 'badge', icon: 'badge',
pages: [ pages: [
{ {
id: guid(), id: 'device_profiles',
name: 'device-profile.device-profiles', name: 'device-profile.device-profiles',
type: 'link', type: 'link',
path: '/profiles/deviceProfiles', path: '/profiles/deviceProfiles',
@ -406,7 +416,7 @@ export class MenuService {
isMdiIcon: true isMdiIcon: true
}, },
{ {
id: guid(), id: 'asset_profiles',
name: 'asset-profile.asset-profiles', name: 'asset-profile.asset-profiles',
type: 'link', type: 'link',
path: '/profiles/assetProfiles', path: '/profiles/assetProfiles',
@ -416,14 +426,14 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'customers',
name: 'customer.customers', name: 'customer.customers',
type: 'link', type: 'link',
path: '/customers', path: '/customers',
icon: 'supervisor_account' icon: 'supervisor_account'
}, },
{ {
id: guid(), id: 'rule_chains',
name: 'rulechain.rulechains', name: 'rulechain.rulechains',
type: 'link', type: 'link',
path: '/ruleChains', path: '/ruleChains',
@ -433,22 +443,24 @@ export class MenuService {
if (authState.edgesSupportEnabled) { if (authState.edgesSupportEnabled) {
sections.push( sections.push(
{ {
id: guid(), id: 'edge_management',
name: 'edge.management', name: 'edge.management',
type: 'toggle', type: 'toggle',
path: '/edgeManagement', path: '/edgeManagement',
icon: 'settings_input_antenna', icon: 'settings_input_antenna',
pages: [ pages: [
{ {
id: guid(), id: 'edges',
name: 'edge.instances', name: 'edge.instances',
fullName: 'edge.edge-instances',
type: 'link', type: 'link',
path: '/edgeManagement/instances', path: '/edgeManagement/instances',
icon: 'router' icon: 'router'
}, },
{ {
id: guid(), id: 'rulechain_templates',
name: 'edge.rulechain-templates', name: 'edge.rulechain-templates',
fullName: 'edge.edge-rulechain-templates',
type: 'link', type: 'link',
path: '/edgeManagement/ruleChains', path: '/edgeManagement/ruleChains',
icon: 'settings_ethernet' icon: 'settings_ethernet'
@ -459,21 +471,21 @@ export class MenuService {
} }
sections.push( sections.push(
{ {
id: guid(), id: 'features',
name: 'feature.advanced-features', name: 'feature.advanced-features',
type: 'toggle', type: 'toggle',
path: '/features', path: '/features',
icon: 'construction', icon: 'construction',
pages: [ pages: [
{ {
id: guid(), id: 'otaUpdates',
name: 'ota-update.ota-updates', name: 'ota-update.ota-updates',
type: 'link', type: 'link',
path: '/features/otaUpdates', path: '/features/otaUpdates',
icon: 'memory' icon: 'memory'
}, },
{ {
id: guid(), id: 'version_control',
name: 'version-control.version-control', name: 'version-control.version-control',
type: 'link', type: 'link',
path: '/features/vc', path: '/features/vc',
@ -482,21 +494,21 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'resources',
name: 'admin.resources', name: 'admin.resources',
type: 'toggle', type: 'toggle',
path: '/resources', path: '/resources',
icon: 'folder', icon: 'folder',
pages: [ pages: [
{ {
id: guid(), id: 'widget_library',
name: 'widget.widget-library', name: 'widget.widget-library',
type: 'link', type: 'link',
path: '/resources/widgets-bundles', path: '/resources/widgets-bundles',
icon: 'now_widgets' icon: 'now_widgets'
}, },
{ {
id: guid(), id: 'resources_library',
name: 'resource.resources-library', name: 'resource.resources-library',
type: 'link', type: 'link',
path: '/resources/resources-library', path: '/resources/resources-library',
@ -506,7 +518,7 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'notifications_center',
name: 'notification.notification-center', name: 'notification.notification-center',
type: 'link', type: 'link',
path: '/notification', path: '/notification',
@ -514,37 +526,42 @@ export class MenuService {
isMdiIcon: true, isMdiIcon: true,
pages: [ pages: [
{ {
id: guid(), id: 'notification_inbox',
name: 'notification.inbox', name: 'notification.inbox',
fullName: 'notification.notification-inbox',
type: 'link', type: 'link',
path: '/notification/inbox', path: '/notification/inbox',
icon: 'inbox' icon: 'inbox'
}, },
{ {
id: guid(), id: 'notification_sent',
name: 'notification.sent', name: 'notification.sent',
fullName: 'notification.notification-sent',
type: 'link', type: 'link',
path: '/notification/sent', path: '/notification/sent',
icon: 'outbox' icon: 'outbox'
}, },
{ {
id: guid(), id: 'notification_recipients',
name: 'notification.recipients', name: 'notification.recipients',
fullName: 'notification.notification-recipients',
type: 'link', type: 'link',
path: '/notification/recipients', path: '/notification/recipients',
icon: 'contacts' icon: 'contacts'
}, },
{ {
id: guid(), id: 'notification_templates',
name: 'notification.templates', name: 'notification.templates',
fullName: 'notification.notification-templates',
type: 'link', type: 'link',
path: '/notification/templates', path: '/notification/templates',
icon: 'mdi:message-draw', icon: 'mdi:message-draw',
isMdiIcon: true isMdiIcon: true
}, },
{ {
id: guid(), id: 'notification_rules',
name: 'notification.rules', name: 'notification.rules',
fullName: 'notification.notification-rules',
type: 'link', type: 'link',
path: '/notification/rules', path: '/notification/rules',
icon: 'mdi:message-cog', icon: 'mdi:message-cog',
@ -553,44 +570,48 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'api_usage',
name: 'api-usage.api-usage', name: 'api-usage.api-usage',
type: 'link', type: 'link',
path: '/usage', path: '/usage',
icon: 'insert_chart' icon: 'insert_chart'
}, },
{ {
id: guid(), id: 'settings',
name: 'admin.settings', name: 'admin.settings',
type: 'link', type: 'link',
path: '/settings', path: '/settings',
icon: 'settings', icon: 'settings',
pages: [ pages: [
{ {
id: guid(), id: 'home_settings',
name: 'admin.home', name: 'admin.home',
fullName: 'admin.home-settings',
type: 'link', type: 'link',
path: '/settings/home', path: '/settings/home',
icon: 'settings_applications' icon: 'settings_applications'
}, },
{ {
id: guid(), id: 'notification_settings',
name: 'admin.notifications', name: 'admin.notifications',
fullName: 'admin.notifications-settings',
type: 'link', type: 'link',
path: '/settings/notifications', path: '/settings/notifications',
icon: 'mdi:message-badge', icon: 'mdi:message-badge',
isMdiIcon: true isMdiIcon: true
}, },
{ {
id: guid(), id: 'repository_settings',
name: 'admin.repository', name: 'admin.repository',
fullName: 'admin.repository-settings',
type: 'link', type: 'link',
path: '/settings/repository', path: '/settings/repository',
icon: 'manage_history' icon: 'manage_history'
}, },
{ {
id: guid(), id: 'auto_commit_settings',
name: 'admin.auto-commit', name: 'admin.auto-commit',
fullName: 'admin.auto-commit-settings',
type: 'link', type: 'link',
path: '/settings/auto-commit', path: '/settings/auto-commit',
icon: 'settings_backup_restore' icon: 'settings_backup_restore'
@ -598,14 +619,14 @@ export class MenuService {
] ]
}, },
{ {
id: guid(), id: 'security_settings',
name: 'security.security', name: 'security.security',
type: 'toggle', type: 'toggle',
path: '/security-settings', path: '/security-settings',
icon: 'security', icon: 'security',
pages: [ pages: [
{ {
id: guid(), id: 'audit_log',
name: 'audit-log.audit-logs', name: 'audit-log.audit-logs',
type: 'link', type: 'link',
path: '/security-settings/auditLogs', path: '/security-settings/auditLogs',
@ -781,49 +802,49 @@ export class MenuService {
const sections: Array<MenuSection> = []; const sections: Array<MenuSection> = [];
sections.push( sections.push(
{ {
id: guid(), id: 'home',
name: 'home.home', name: 'home.home',
type: 'link', type: 'link',
path: '/home', path: '/home',
icon: 'home' icon: 'home'
}, },
{ {
id: guid(), id: 'alarms',
name: 'alarm.alarms', name: 'alarm.alarms',
type: 'link', type: 'link',
path: '/alarms', path: '/alarms',
icon: 'notifications' icon: 'notifications'
}, },
{ {
id: guid(), id: 'dashboards',
name: 'dashboard.dashboards', name: 'dashboard.dashboards',
type: 'link', type: 'link',
path: '/dashboards', path: '/dashboards',
icon: 'dashboards' icon: 'dashboards'
}, },
{ {
id: guid(), id: 'entities',
name: 'entity.entities', name: 'entity.entities',
type: 'toggle', type: 'toggle',
path: '/entities', path: '/entities',
icon: 'category', icon: 'category',
pages: [ pages: [
{ {
id: guid(), id: 'devices',
name: 'device.devices', name: 'device.devices',
type: 'link', type: 'link',
path: '/entities/devices', path: '/entities/devices',
icon: 'devices_other' icon: 'devices_other'
}, },
{ {
id: guid(), id: 'assets',
name: 'asset.assets', name: 'asset.assets',
type: 'link', type: 'link',
path: '/entities/assets', path: '/entities/assets',
icon: 'domain' icon: 'domain'
}, },
{ {
id: guid(), id: 'entity_views',
name: 'entity-view.entity-views', name: 'entity-view.entity-views',
type: 'link', type: 'link',
path: '/entities/entityViews', path: '/entities/entityViews',
@ -835,8 +856,9 @@ export class MenuService {
if (authState.edgesSupportEnabled) { if (authState.edgesSupportEnabled) {
sections.push( sections.push(
{ {
id: guid(), id: 'edges',
name: 'edge.edge-instances', name: 'edge.edge-instances',
fullName: 'edge.edge-instances',
type: 'link', type: 'link',
path: '/edgeManagement/instances', path: '/edgeManagement/instances',
icon: 'router' icon: 'router'
@ -845,7 +867,7 @@ export class MenuService {
} }
sections.push( sections.push(
{ {
id: guid(), id: 'notifications_center',
name: 'notification.notification-center', name: 'notification.notification-center',
type: 'link', type: 'link',
path: '/notification', path: '/notification',
@ -853,8 +875,9 @@ export class MenuService {
isMdiIcon: true, isMdiIcon: true,
pages: [ pages: [
{ {
id: guid(), id: 'notification_inbox',
name: 'notification.inbox', name: 'notification.inbox',
fullName: 'notification.notification-inbox',
type: 'link', type: 'link',
path: '/notification/inbox', path: '/notification/inbox',
icon: 'inbox' icon: 'inbox'
@ -928,6 +951,19 @@ export class MenuService {
return homeSections; return homeSections;
} }
private allMenuLinks(sections: Array<MenuSection>): Array<MenuSection> {
const result: Array<MenuSection> = [];
for (const section of sections) {
if (section.type === 'link') {
result.push(section);
}
if (section.pages && section.pages.length) {
result.push(...this.allMenuLinks(section.pages));
}
}
return result;
}
public menuSections(): Observable<Array<MenuSection>> { public menuSections(): Observable<Array<MenuSection>> {
return this.menuSections$; return this.menuSections$;
} }
@ -936,5 +972,24 @@ export class MenuService {
return this.homeSections$; return this.homeSections$;
} }
} public availableMenuLinks(): Observable<Array<MenuSection>> {
return this.availableMenuLinks$;
}
public menuLinkById(id: string): Observable<MenuSection | undefined> {
return this.availableMenuLinks$.pipe(
map((links) => links.find(link => link.id === id))
);
}
public menuLinksByIds(ids: string[]): Observable<Array<MenuSection>> {
return this.availableMenuLinks$.pipe(
map((links) => links.filter(link => ids.includes(link.id)).sort((a, b) => {
const i1 = ids.indexOf(a.id);
const i2 = ids.indexOf(b.id);
return i1 - i2;
}))
);
}
}

15
ui-ngx/src/app/core/ws/telemetry-websocket.service.ts

@ -16,6 +16,8 @@
import { Inject, Injectable, NgZone } from '@angular/core'; import { Inject, Injectable, NgZone } from '@angular/core';
import { import {
AlarmCountCmd, AlarmCountUnsubscribeCmd,
AlarmCountUpdate,
AlarmDataCmd, AlarmDataCmd,
AlarmDataUnsubscribeCmd, AlarmDataUnsubscribeCmd,
AlarmDataUpdate, AlarmDataUpdate,
@ -26,7 +28,7 @@ import {
EntityDataCmd, EntityDataCmd,
EntityDataUnsubscribeCmd, EntityDataUnsubscribeCmd,
EntityDataUpdate, EntityDataUpdate,
GetHistoryCmd, GetHistoryCmd, isAlarmCountUpdateMsg,
isAlarmDataUpdateMsg, isAlarmDataUpdateMsg,
isEntityCountUpdateMsg, isEntityCountUpdateMsg,
isEntityDataUpdateMsg, isEntityDataUpdateMsg,
@ -80,6 +82,8 @@ export class TelemetryWebsocketService extends WebsocketService<TelemetrySubscri
this.cmdWrapper.alarmDataCmds.push(subscriptionCommand); this.cmdWrapper.alarmDataCmds.push(subscriptionCommand);
} else if (subscriptionCommand instanceof EntityCountCmd) { } else if (subscriptionCommand instanceof EntityCountCmd) {
this.cmdWrapper.entityCountCmds.push(subscriptionCommand); this.cmdWrapper.entityCountCmds.push(subscriptionCommand);
} else if (subscriptionCommand instanceof AlarmCountCmd) {
this.cmdWrapper.alarmCountCmds.push(subscriptionCommand);
} }
} }
); );
@ -123,6 +127,10 @@ export class TelemetryWebsocketService extends WebsocketService<TelemetrySubscri
const entityCountUnsubscribeCmd = new EntityCountUnsubscribeCmd(); const entityCountUnsubscribeCmd = new EntityCountUnsubscribeCmd();
entityCountUnsubscribeCmd.cmdId = subscriptionCommand.cmdId; entityCountUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
this.cmdWrapper.entityCountUnsubscribeCmds.push(entityCountUnsubscribeCmd); this.cmdWrapper.entityCountUnsubscribeCmds.push(entityCountUnsubscribeCmd);
} else if (subscriptionCommand instanceof AlarmCountCmd) {
const alarmCountUnsubscribeCmd = new AlarmCountUnsubscribeCmd();
alarmCountUnsubscribeCmd.cmdId = subscriptionCommand.cmdId;
this.cmdWrapper.alarmCountUnsubscribeCmds.push(alarmCountUnsubscribeCmd);
} }
const cmdId = subscriptionCommand.cmdId; const cmdId = subscriptionCommand.cmdId;
if (cmdId) { if (cmdId) {
@ -153,6 +161,11 @@ export class TelemetryWebsocketService extends WebsocketService<TelemetrySubscri
if (subscriber) { if (subscriber) {
subscriber.onEntityCount(new EntityCountUpdate(message)); subscriber.onEntityCount(new EntityCountUpdate(message));
} }
} else if (isAlarmCountUpdateMsg(message)) {
subscriber = this.subscribersMap.get(message.cmdId);
if (subscriber) {
subscriber.onAlarmCount(new AlarmCountUpdate(message));
}
} else if (message.subscriptionId) { } else if (message.subscriptionId) {
subscriber = this.subscribersMap.get(message.subscriptionId); subscriber = this.subscribersMap.get(message.subscriptionId);
if (subscriber) { if (subscriber) {

2
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html

@ -28,7 +28,7 @@
(optionSelected)="selected($event)"> (optionSelected)="selected($event)">
<mat-option [fxHide]="!assigneeId" [value]="null"> <mat-option [fxHide]="!assigneeId" [value]="null">
<mat-icon class="unassigned-icon">account_circle</mat-icon> <mat-icon class="unassigned-icon">account_circle</mat-icon>
<span translate>alarm.unassigned</span> <span>{{ assigneeNotSetText | translate }}</span>
</mat-option> </mat-option>
<mat-option *ngFor="let user of filteredUsers | async" [value]="user" <mat-option *ngFor="let user of filteredUsers | async" [value]="user"
[fxHide]="assigneeId === user.id.id"> [fxHide]="assigneeId === user.id.id">

2
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts

@ -65,6 +65,8 @@ export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDe
assigneeId?: string; assigneeId?: string;
assigneeNotSetText = 'alarm.unassigned';
reassigned: boolean = false; reassigned: boolean = false;
selectUserFormGroup: FormGroup; selectUserFormGroup: FormGroup;

203
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select-panel.component.ts

@ -0,0 +1,203 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
Component,
ElementRef,
Inject,
InjectionToken,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Observable, of, Subject } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, takeUntil, } from 'rxjs/operators';
import { User, UserEmailInfo } from '@shared/models/user.model';
import { TranslateService } from '@ngx-translate/core';
import { UserService } from '@core/http/user.service';
import { PageLink } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order';
import { emptyPageData } from '@shared/models/page/page-data';
import { OverlayRef } from '@angular/cdk/overlay';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { UtilsService } from '@core/services/utils.service';
export const ALARM_ASSIGNEE_SELECT_PANEL_DATA = new InjectionToken<any>('AlarmAssigneeSelectPanelData');
export interface AlarmAssigneeSelectPanelData {
assigneeId?: string;
}
@Component({
selector: 'tb-alarm-assignee-select-panel',
templateUrl: './alarm-assignee-panel.component.html',
styleUrls: ['./alarm-assignee-panel.component.scss']
})
export class AlarmAssigneeSelectPanelComponent implements OnInit, AfterViewInit, OnDestroy {
private dirty = false;
assigneeId?: string;
assigneeNotSetText = 'alarm.assignee-not-set';
selectUserFormGroup: FormGroup;
@ViewChild('userInput', {static: true}) userInput: ElementRef;
filteredUsers: Observable<Array<UserEmailInfo>>;
searchText = '';
userSelected = false;
result?: UserEmailInfo;
private destroy$ = new Subject<void>();
constructor(@Inject(ALARM_ASSIGNEE_SELECT_PANEL_DATA) public data: AlarmAssigneeSelectPanelData,
public overlayRef: OverlayRef,
public translate: TranslateService,
private userService: UserService,
private fb: FormBuilder,
private utilsService: UtilsService) {
this.assigneeId = data.assigneeId;
this.selectUserFormGroup = this.fb.group({
user: [null]
});
}
ngOnInit() {
this.filteredUsers = this.selectUserFormGroup.get('user').valueChanges
.pipe(
debounceTime(150),
map(value => value ? (typeof value === 'string' ? value : '') : ''),
distinctUntilChanged(),
switchMap(name => this.fetchUsers(name)),
share(),
takeUntil(this.destroy$)
);
}
ngAfterViewInit() {
setTimeout(() => {
this.userInput.nativeElement.focus();
}, 0);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
displayUserFn(user?: User): string | undefined {
return user ? user.email : undefined;
}
selected(event: MatAutocompleteSelectedEvent): void {
this.clear();
this.userSelected = true;
this.result = event.option.value;
this.overlayRef.dispose();
}
fetchUsers(searchText?: string): Observable<Array<UserEmailInfo>> {
this.searchText = searchText;
const pageLink = new PageLink(50, 0, searchText, {
property: 'email',
direction: Direction.ASC
});
return this.userService.findUsersByQuery(pageLink, {ignoreLoading: true})
.pipe(
catchError(() => of(emptyPageData<UserEmailInfo>())),
map(pageData => pageData.data)
);
}
onFocus(): void {
if (!this.dirty) {
this.selectUserFormGroup.get('user').updateValueAndValidity({onlySelf: true});
this.dirty = true;
}
}
clear() {
this.selectUserFormGroup.get('user').patchValue('', {emitEvent: true});
setTimeout(() => {
this.userInput.nativeElement.blur();
this.userInput.nativeElement.focus();
}, 0);
}
getUserDisplayName(entity: UserEmailInfo) {
let displayName = '';
if ((entity.firstName && entity.firstName.length > 0) ||
(entity.lastName && entity.lastName.length > 0)) {
if (entity.firstName) {
displayName += entity.firstName;
}
if (entity.lastName) {
if (displayName.length > 0) {
displayName += ' ';
}
displayName += entity.lastName;
}
} else {
displayName = entity.email;
}
return displayName;
}
getUserInitials(entity: UserEmailInfo): string {
let initials = '';
if (entity.firstName && entity.firstName.length ||
entity.lastName && entity.lastName.length) {
if (entity.firstName) {
initials += entity.firstName.charAt(0);
}
if (entity.lastName) {
initials += entity.lastName.charAt(0);
}
} else {
initials += entity.email.charAt(0);
}
return initials.toUpperCase();
}
getFullName(entity: UserEmailInfo): string {
let fullName = '';
if ((entity.firstName && entity.firstName.length > 0) ||
(entity.lastName && entity.lastName.length > 0)) {
if (entity.firstName) {
fullName += entity.firstName;
}
if (entity.lastName) {
if (fullName.length > 0) {
fullName += ' ';
}
fullName += entity.lastName;
}
}
return fullName;
}
getAvatarBgColor(entity: UserEmailInfo) {
return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60);
}
}

31
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html

@ -0,0 +1,31 @@
<!--
Copyright © 2016-2023 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.
-->
<ng-container [formGroup]="assigneeFormGroup">
<mat-form-field fxFlex class="mat-block" style="margin-bottom: 25px"
(click)="openAlarmAssigneeSelectPanel($event)"
subscriptSizing="dynamic">
<mat-label translate>alarm.assignee</mat-label>
<input matInput readonly formControlName="assignee">
<span *ngIf="assignee" matPrefix class="user-avatar"
[style.backgroundColor]="getAvatarBgColor()">
{{ getUserInitials() }}
</span>
<mat-icon *ngIf="!assignee" matPrefix class="unassigned-icon">account_circle</mat-icon>
<mat-icon *ngIf="!disabled" matSuffix>arrow_drop_down</mat-icon>
</mat-form-field>
</ng-container>

193
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.ts

@ -0,0 +1,193 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Injector, Input, OnInit, StaticProvider, ViewContainerRef } from '@angular/core';
import { UtilsService } from '@core/services/utils.service';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { TranslateService } from '@ngx-translate/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { UserId } from '@shared/models/id/user-id';
import { UserService } from '@core/http/user.service';
import { User, UserEmailInfo } from '@shared/models/user.model';
import { catchError, map, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import {
ALARM_ASSIGNEE_SELECT_PANEL_DATA,
AlarmAssigneeSelectPanelComponent,
AlarmAssigneeSelectPanelData
} from '@home/components/alarm/alarm-assignee-select-panel.component';
@Component({
selector: 'tb-alarm-assignee-select',
templateUrl: './alarm-assignee-select.component.html',
styleUrls: ['./alarm-assignee.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AlarmAssigneeSelectComponent),
multi: true
}
]
})
export class AlarmAssigneeSelectComponent implements OnInit, ControlValueAccessor {
@Input() disabled: boolean;
assigneeFormGroup: UntypedFormGroup;
assignee?: User | UserEmailInfo;
private propagateChange = (_: any) => {};
constructor(private utilsService: UtilsService,
private overlay: Overlay,
private fb: UntypedFormBuilder,
private userService: UserService,
private viewContainerRef: ViewContainerRef,
private translateService: TranslateService) {
}
ngOnInit(): void {
this.assigneeFormGroup = this.fb.group({
assignee: [null, []]
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.assigneeFormGroup.disable({emitEvent: false});
} else {
this.assigneeFormGroup.enable({emitEvent: false});
}
}
writeValue(userId?: UserId): void {
const userObservable = userId ? this.userService.getUser(userId.id, {ignoreErrors: true}).pipe(
catchError(() => of(null))
) : of(null);
userObservable.pipe(
tap((user) => {
this.assignee = user;
}),
map((user) => this.getAssignee(user))
).subscribe((assignee) => {
this.assigneeFormGroup.get('assignee').patchValue(assignee, {emitEvent: false});
});
}
private getAssignee(user?: User| UserEmailInfo): string {
if (user) {
return this.getUserDisplayName(user);
} else {
return this.translateService.instant('alarm.assignee-not-set');
}
}
private getUserDisplayName(user?: User | UserEmailInfo): string {
let displayName = '';
if ((user?.firstName && user?.firstName.length > 0) ||
(user?.lastName && user?.lastName.length > 0)) {
if (user?.firstName) {
displayName += user?.firstName;
}
if (user?.lastName) {
if (displayName.length > 0) {
displayName += ' ';
}
displayName += user?.lastName;
}
} else {
displayName = user?.email;
}
return displayName;
}
getUserInitials(): string {
let initials = '';
if (this.assignee?.firstName && this.assignee?.firstName.length ||
this.assignee?.lastName && this.assignee?.lastName.length) {
if (this.assignee?.firstName) {
initials += this.assignee?.firstName.charAt(0);
}
if (this.assignee?.lastName) {
initials += this.assignee?.lastName.charAt(0);
}
} else {
initials += this.assignee?.email.charAt(0);
}
return initials.toUpperCase();
}
getAvatarBgColor(): string {
return this.utilsService.stringToHslColor(this.getUserDisplayName(this.assignee), 40, 60);
}
openAlarmAssigneeSelectPanel($event: Event) {
if ($event) {
$event.stopPropagation();
}
if (!this.disabled) {
const target = $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'center',
originY: 'bottom',
overlayX: 'center',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
config.width = (target as HTMLElement).offsetWidth;
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const providers: StaticProvider[] = [
{
provide: ALARM_ASSIGNEE_SELECT_PANEL_DATA,
useValue: {
assigneeId: this.assignee?.id?.id
} as AlarmAssigneeSelectPanelData
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
const component = overlayRef.attach(new ComponentPortal(AlarmAssigneeSelectPanelComponent,
this.viewContainerRef, injector));
component.onDestroy(() => {
if (component.instance.userSelected) {
this.assignee = component.instance.result;
this.assigneeFormGroup.get('assignee').patchValue(this.getAssignee(this.assignee), {emitEvent: false});
this.propagateChange(this.assignee?.id);
}
});
}
}
}

103
ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html

@ -0,0 +1,103 @@
<!--
Copyright © 2016-2023 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.
-->
<ng-container *ngIf="panelMode; else componentMode">
<ng-container *ngTemplateOutlet="alarmFilterPanel"></ng-container>
</ng-container>
<ng-template #componentMode>
<ng-container *ngIf="buttonMode; else alarmFilter">
<button color="primary"
matTooltip="{{buttonDisplayValue}}"
matTooltipPosition="above"
mat-stroked-button
(click)="toggleAlarmFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}}
</button>
</ng-container>
</ng-template>
<ng-template #alarmFilterPanel>
<form fxLayout="column" class="mat-content mat-padding" (ngSubmit)="update()">
<ng-container *ngTemplateOutlet="alarmFilter"></ng-container>
<div fxLayout="row" class="tb-panel-actions" fxLayoutAlign="end center">
<button type="button"
mat-button
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button type="submit"
mat-raised-button
color="primary"
[disabled]="alarmFilterConfigForm.invalid || !alarmFilterConfigForm.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>
</ng-template>
<ng-template #alarmFilter>
<div fxLayout="column" fxLayoutAlign="center" [formGroup]="alarmFilterConfigForm">
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start start"
fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-status-list</mat-label>
<mat-select formControlName="statusList" multiple
placeholder="{{ !alarmFilterConfigForm.get('statusList').value?.length ? ('alarm.any-status' | translate) : '' }}">
<mat-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus">
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-severity-list</mat-label>
<mat-select formControlName="severityList" multiple
placeholder="{{ !alarmFilterConfigForm.get('severityList').value?.length ? ('alarm.any-severity' | translate) : '' }}">
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="start start"
fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>alarm.alarm-type-list</mat-label>
<mat-chip-grid #alarmTypeChipList formControlName="typeList">
<mat-chip-row *ngFor="let type of alarmTypeList()"
[removable]="true" (removed)="removeAlarmType(type)">
{{type}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip-row>
<input placeholder="{{ !alarmFilterConfigForm.get('typeList').value?.length ? ('alarm.any-type' | translate) : '' }}"
[matChipInputFor]="alarmTypeChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addAlarmType($event)">
</mat-chip-grid>
</mat-form-field>
</div>
<div fxLayout="column" fxLayoutGap="16px">
<mat-slide-toggle *ngIf="propagatedFilter" formControlName="searchPropagatedAlarms">
{{ 'alarm.search-propagated-alarms' | translate }}
</mat-slide-toggle>
<mat-slide-toggle formControlName="assignedToCurrentUser">
{{ (userMode ? 'alarm.assigned-to-me' : 'alarm.assigned-to-current-user') | translate }}
</mat-slide-toggle>
<tb-alarm-assignee-select
*ngIf="!alarmFilterConfigForm.get('assignedToCurrentUser').value" formControlName="assigneeId">
</tb-alarm-assignee-select>
</div>
</div>
</ng-template>

29
ui-ngx/src/app/modules/home/components/widget/lib/alarm-filter-panel.component.scss → ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.scss

@ -14,23 +14,22 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
width: 100%; display: block;
height: 100%;
min-width: 300px;
overflow: hidden; overflow: hidden;
background: #fff; max-width: 100%;
border-radius: 4px; .mdc-button {
box-shadow: max-width: 100%;
0 7px 8px -4px rgba(0, 0, 0, .2),
0 13px 19px 2px rgba(0, 0, 0, .14),
0 5px 24px 4px rgba(0, 0, 0, .12);
.mat-content {
overflow: hidden;
background-color: #fff;
} }
}
.mat-padding { :host ::ng-deep {
padding: 16px; .mdc-button {
.mat-icon {
min-width: 24px;
}
.mdc-button__label {
overflow: hidden;
text-overflow: ellipsis;
}
} }
} }

318
ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts

@ -0,0 +1,318 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Component,
ElementRef,
forwardRef,
Inject,
InjectionToken,
Input,
OnDestroy,
OnInit,
Optional,
TemplateRef,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { AlarmFilterConfig } from '@shared/models/query/query.models';
import { coerceBoolean } from '@shared/decorators/coerce-boolean';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import {
AlarmSearchStatus,
alarmSearchStatusTranslations,
AlarmSeverity,
alarmSeverityTranslations
} from '@shared/models/alarm.models';
import { MatChipInputEvent } from '@angular/material/chips';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { TranslateService } from '@ngx-translate/core';
export const ALARM_FILTER_CONFIG_DATA = new InjectionToken<any>('AlarmFilterConfigData');
export interface AlarmFilterConfigData {
panelMode: boolean;
userMode: boolean;
alarmFilterConfig: AlarmFilterConfig;
}
// @dynamic
@Component({
selector: 'tb-alarm-filter-config',
templateUrl: './alarm-filter-config.component.html',
styleUrls: ['./alarm-filter-config.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AlarmFilterConfigComponent),
multi: true
}
]
})
export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlValueAccessor {
@ViewChild('alarmFilterPanel')
alarmFilterPanel: TemplateRef<any>;
@Input() disabled: boolean;
@coerceBoolean()
@Input()
buttonMode = true;
@coerceBoolean()
@Input()
userMode = false;
@coerceBoolean()
@Input()
propagatedFilter = true;
panelMode = false;
readonly separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON];
alarmSearchStatuses = [AlarmSearchStatus.ACTIVE,
AlarmSearchStatus.CLEARED,
AlarmSearchStatus.ACK,
AlarmSearchStatus.UNACK];
alarmSearchStatusTranslationMap = alarmSearchStatusTranslations;
alarmSeverities = Object.keys(AlarmSeverity);
alarmSeverityEnum = AlarmSeverity;
alarmSeverityTranslationMap = alarmSeverityTranslations;
buttonDisplayValue = this.translate.instant('alarm.alarm-filter');
alarmFilterConfigForm: UntypedFormGroup;
alarmFilterOverlayRef: OverlayRef;
panelResult: AlarmFilterConfig = null;
private alarmFilterConfig: AlarmFilterConfig;
private propagateChange = (_: any) => {};
constructor(@Optional() @Inject(ALARM_FILTER_CONFIG_DATA)
private data: AlarmFilterConfigData | undefined,
@Optional()
private overlayRef: OverlayRef,
private fb: UntypedFormBuilder,
private translate: TranslateService,
private overlay: Overlay,
private nativeElement: ElementRef,
private viewContainerRef: ViewContainerRef) {
}
ngOnInit(): void {
if (this.data) {
this.panelMode = this.data.panelMode;
this.userMode = this.data.userMode;
this.alarmFilterConfig = this.data.alarmFilterConfig;
}
this.alarmFilterConfigForm = this.fb.group({
statusList: [null, []],
severityList: [null, []],
typeList: [null, []],
searchPropagatedAlarms: [false, []],
assignedToCurrentUser: [false, []],
assigneeId: [null, []]
});
this.alarmFilterConfigForm.valueChanges.subscribe(
() => {
this.updateValidators();
if (!this.buttonMode) {
this.alarmConfigUpdated(this.alarmFilterConfigForm.value);
}
}
);
if (this.panelMode) {
this.updateAlarmConfigForm(this.alarmFilterConfig);
}
}
ngOnDestroy(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.alarmFilterConfigForm.disable({emitEvent: false});
} else {
this.alarmFilterConfigForm.enable({emitEvent: false});
this.updateValidators();
}
}
writeValue(alarmFilterConfig?: AlarmFilterConfig): void {
this.alarmFilterConfig = alarmFilterConfig;
this.updateButtonDisplayValue();
this.updateAlarmConfigForm(alarmFilterConfig);
}
private updateValidators() {
const assignedToCurrentUser = this.alarmFilterConfigForm.get('assignedToCurrentUser').value;
if (assignedToCurrentUser) {
this.alarmFilterConfigForm.get('assigneeId').disable({emitEvent: false});
} else {
this.alarmFilterConfigForm.get('assigneeId').enable({emitEvent: false});
}
this.alarmFilterConfigForm.get('assigneeId').updateValueAndValidity({emitEvent: false});
}
toggleAlarmFilterPanel($event: Event) {
if ($event) {
$event.stopPropagation();
}
const config = new OverlayConfig({
panelClass: 'tb-alarm-filter-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
maxHeight: '80vh',
height: 'min-content',
minWidth: ''
});
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(this.nativeElement)
.withPositions([connectedPosition]);
this.alarmFilterOverlayRef = this.overlay.create(config);
this.alarmFilterOverlayRef.backdropClick().subscribe(() => {
this.alarmFilterOverlayRef.dispose();
});
this.alarmFilterOverlayRef.attach(new TemplatePortal(this.alarmFilterPanel,
this.viewContainerRef));
}
cancel() {
this.updateAlarmConfigForm(this.alarmFilterConfig);
if (this.overlayRef) {
this.overlayRef.dispose();
} else {
this.alarmFilterOverlayRef.dispose();
}
}
update() {
this.alarmConfigUpdated(this.alarmFilterConfigForm.value);
if (this.panelMode) {
this.panelResult = this.alarmFilterConfig;
}
if (this.overlayRef) {
this.overlayRef.dispose();
} else {
this.alarmFilterOverlayRef.dispose();
}
}
public alarmTypeList(): string[] {
return this.alarmFilterConfigForm.get('typeList').value;
}
public removeAlarmType(type: string): void {
const types: string[] = this.alarmFilterConfigForm.get('typeList').value;
const index = types.indexOf(type);
if (index >= 0) {
types.splice(index, 1);
this.alarmFilterConfigForm.get('typeList').setValue(types);
this.alarmFilterConfigForm.get('typeList').markAsDirty();
}
}
public addAlarmType(event: MatChipInputEvent): void {
const input = event.chipInput.inputElement;
const value = event.value;
let types: string[] = this.alarmFilterConfigForm.get('typeList').value;
if ((value || '').trim()) {
if (!types) {
types = [];
}
types.push(value.trim());
this.alarmFilterConfigForm.get('typeList').setValue(types);
this.alarmFilterConfigForm.get('typeList').markAsDirty();
}
if (input) {
input.value = '';
}
}
private updateAlarmConfigForm(alarmFilterConfig?: AlarmFilterConfig) {
this.alarmFilterConfigForm.patchValue({
statusList: alarmFilterConfig?.statusList,
severityList: alarmFilterConfig?.severityList,
typeList: alarmFilterConfig?.typeList,
searchPropagatedAlarms: alarmFilterConfig?.searchPropagatedAlarms,
assignedToCurrentUser: alarmFilterConfig?.assignedToCurrentUser,
assigneeId: alarmFilterConfig?.assigneeId
}, {emitEvent: false});
this.updateValidators();
}
private alarmConfigUpdated(alarmFilterConfig: AlarmFilterConfig) {
this.alarmFilterConfig = alarmFilterConfig;
this.updateButtonDisplayValue();
this.propagateChange(this.alarmFilterConfig);
}
private updateButtonDisplayValue() {
if (this.buttonMode) {
const filterTextParts: string[] = [];
if (this.alarmFilterConfig?.statusList?.length) {
filterTextParts.push(this.alarmFilterConfig.statusList.map(s =>
this.translate.instant(alarmSearchStatusTranslations.get(s))).join(', '));
}
if (this.alarmFilterConfig?.severityList?.length) {
filterTextParts.push(this.alarmFilterConfig.severityList.map(s =>
this.translate.instant(alarmSeverityTranslations.get(s))).join(', '));
}
if (this.alarmFilterConfig?.typeList?.length) {
filterTextParts.push(this.alarmFilterConfig.typeList.join(', '));
}
if (this.alarmFilterConfig?.assignedToCurrentUser) {
filterTextParts.push(this.translate.instant('alarm.assigned-to-me'));
} else if (this.alarmFilterConfig?.assigneeId) {
filterTextParts.push(this.translate.instant('alarm.assigned'));
}
if (!filterTextParts.length) {
this.buttonDisplayValue = this.translate.instant('alarm.alarm-filter-title');
} else {
this.buttonDisplayValue = this.translate.instant('alarm.filter-title') + `: ${filterTextParts.join(', ')}`;
}
}
}
}

22
ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts

@ -32,6 +32,7 @@ import { EntityId } from '@shared/models/id/entity-id';
import { import {
AlarmInfo, AlarmInfo,
AlarmQuery, AlarmQuery,
AlarmQueryV2,
AlarmSearchStatus, AlarmSearchStatus,
alarmSeverityColors, alarmSeverityColors,
alarmSeverityTranslations, alarmSeverityTranslations,
@ -45,7 +46,7 @@ import {
AlarmDetailsDialogComponent, AlarmDetailsDialogComponent,
AlarmDetailsDialogData AlarmDetailsDialogData
} from '@home/components/alarm/alarm-details-dialog.component'; } from '@home/components/alarm/alarm-details-dialog.component';
import { DAY, historyInterval } from '@shared/models/time/time.models'; import { DAY, forAllTimeInterval, historyInterval } from '@shared/models/time/time.models';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
@ -60,21 +61,24 @@ import {
import { ComponentPortal } from '@angular/cdk/portal'; import { ComponentPortal } from '@angular/cdk/portal';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { AlarmFilterConfig } from '@shared/models/query/query.models';
import { EntityService } from '@core/http/entity.service';
export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink> { export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink> {
private authUser = getCurrentAuthUser(this.store); private authUser = getCurrentAuthUser(this.store);
searchStatus: AlarmSearchStatus; alarmFilterConfig: AlarmFilterConfig;
constructor(private alarmService: AlarmService, constructor(private alarmService: AlarmService,
private entityService: EntityService,
private dialogService: DialogService, private dialogService: DialogService,
private translate: TranslateService, private translate: TranslateService,
private datePipe: DatePipe, private datePipe: DatePipe,
private dialog: MatDialog, private dialog: MatDialog,
private alarmsMode: AlarmsMode = AlarmsMode.ALL, private alarmsMode: AlarmsMode = AlarmsMode.ALL,
public entityId: EntityId = null, public entityId: EntityId = null,
private defaultSearchStatus: AlarmSearchStatus = AlarmSearchStatus.ANY, private defaultAlarmFilterConfig: AlarmFilterConfig = {statusList: [AlarmSearchStatus.ACTIVE]},
private store: Store<AppState>, private store: Store<AppState>,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private overlay: Overlay, private overlay: Overlay,
@ -85,8 +89,9 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
this.loadDataOnInit = false; this.loadDataOnInit = false;
this.tableTitle = ''; this.tableTitle = '';
this.useTimePageLink = true; this.useTimePageLink = true;
this.forAllTimeEnabled = true;
this.pageMode = pageMode; this.pageMode = pageMode;
this.defaultTimewindowInterval = historyInterval(DAY * 30); this.defaultTimewindowInterval = forAllTimeInterval();
this.detailsPanelEnabled = false; this.detailsPanelEnabled = false;
this.selectionEnabled = false; this.selectionEnabled = false;
this.searchEnabled = true; this.searchEnabled = true;
@ -97,7 +102,7 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
this.entityTranslations = entityTypeTranslations.get(EntityType.ALARM); this.entityTranslations = entityTypeTranslations.get(EntityType.ALARM);
this.entityResources = { this.entityResources = {
} as EntityTypeResource<AlarmInfo>; } as EntityTypeResource<AlarmInfo>;
this.searchStatus = defaultSearchStatus; this.alarmFilterConfig = defaultAlarmFilterConfig;
this.headerComponent = AlarmTableHeaderComponent; this.headerComponent = AlarmTableHeaderComponent;
@ -152,12 +157,13 @@ export class AlarmTableConfig extends EntityTableConfig<AlarmInfo, TimePageLink>
} }
fetchAlarms(pageLink: TimePageLink): Observable<PageData<AlarmInfo>> { fetchAlarms(pageLink: TimePageLink): Observable<PageData<AlarmInfo>> {
const query = new AlarmQuery(this.entityId, pageLink, this.searchStatus, null, true, null); const alarmFilter = this.entityService.resolveAlarmFilter(this.alarmFilterConfig, false);
const query = new AlarmQueryV2(this.entityId, pageLink, alarmFilter);
switch (this.alarmsMode) { switch (this.alarmsMode) {
case AlarmsMode.ALL: case AlarmsMode.ALL:
return this.alarmService.getAllAlarms(query); return this.alarmService.getAllAlarmsV2(query);
case AlarmsMode.ENTITY: case AlarmsMode.ENTITY:
return this.alarmService.getAlarms(query); return this.alarmService.getAlarmsV2(query);
} }
} }

14
ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.html

@ -15,12 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field class="mat-block"> <tb-alarm-filter-config [ngModel]="alarmTableConfig.alarmFilterConfig"
<mat-label translate>alarm.alarm-status</mat-label> (ngModelChange)="alarmFilterChanged($event)"
<mat-select [ngModel]="alarmTableConfig.searchStatus" propagatedFilter="false"
(ngModelChange)="searchStatusChanged($event)"> userMode>
<mat-option *ngFor="let status of alarmSearchStatusTypes" [value]="status"> </tb-alarm-filter-config>
{{ alarmSearchStatusTranslationsMap.get(alarmSearchStatusEnum[status]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>

6
ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.scss

@ -16,9 +16,7 @@
@import '../../../../../scss/constants'; @import '../../../../../scss/constants';
:host { :host {
/* flex: 1;
display: flex;
justify-content: flex-start; */
padding-right: 8px; padding-right: 8px;
min-width: 160px; overflow: hidden;
max-width: 100%;
} }

10
ui-ngx/src/app/modules/home/components/alarm/alarm-table-header.component.ts

@ -20,6 +20,7 @@ import { AppState } from '@core/core.state';
import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component'; import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component';
import { AlarmInfo, AlarmSearchStatus, alarmSearchStatusTranslations } from '@shared/models/alarm.models'; import { AlarmInfo, AlarmSearchStatus, alarmSearchStatusTranslations } from '@shared/models/alarm.models';
import { AlarmTableConfig } from './alarm-table-config'; import { AlarmTableConfig } from './alarm-table-config';
import { AlarmFilterConfig } from '@shared/models/query/query.models';
@Component({ @Component({
selector: 'tb-alarm-table-header', selector: 'tb-alarm-table-header',
@ -28,11 +29,6 @@ import { AlarmTableConfig } from './alarm-table-config';
}) })
export class AlarmTableHeaderComponent extends EntityTableHeaderComponent<AlarmInfo> { export class AlarmTableHeaderComponent extends EntityTableHeaderComponent<AlarmInfo> {
alarmSearchStatusTranslationsMap = alarmSearchStatusTranslations;
alarmSearchStatusTypes = Object.keys(AlarmSearchStatus);
alarmSearchStatusEnum = AlarmSearchStatus;
get alarmTableConfig(): AlarmTableConfig { get alarmTableConfig(): AlarmTableConfig {
return this.entitiesTableConfig as AlarmTableConfig; return this.entitiesTableConfig as AlarmTableConfig;
} }
@ -41,8 +37,8 @@ export class AlarmTableHeaderComponent extends EntityTableHeaderComponent<AlarmI
super(store); super(store);
} }
searchStatusChanged(searchStatus: AlarmSearchStatus) { alarmFilterChanged(alarmFilterConfig: AlarmFilterConfig) {
this.alarmTableConfig.searchStatus = searchStatus; this.alarmTableConfig.alarmFilterConfig = alarmFilterConfig;
this.alarmTableConfig.getTable().resetSortAndFilter(true, true); this.alarmTableConfig.getTable().resetSortAndFilter(true, true);
} }
} }

61
ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.ts

@ -22,14 +22,24 @@ import { EntityId } from '@shared/models/id/entity-id';
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { AlarmTableConfig } from './alarm-table-config'; import { AlarmTableConfig } from './alarm-table-config';
import { AlarmSearchStatus, AlarmsMode } from '@shared/models/alarm.models'; import { AlarmSearchStatus, AlarmSeverity, AlarmsMode } from '@shared/models/alarm.models';
import { AlarmService } from '@app/core/http/alarm.service'; import { AlarmService } from '@app/core/http/alarm.service';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Overlay } from '@angular/cdk/overlay'; import { Overlay } from '@angular/cdk/overlay';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { isDefinedAndNotNull } from '@core/utils'; import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { EntityService } from '@core/http/entity.service';
import { PageQueryParam } from '@shared/models/page/page-link';
import { AlarmFilterConfig } from '@shared/models/query/query.models';
interface AlarmPageQueryParams extends PageQueryParam {
typeList?: Array<string>;
statusList?: Array<AlarmSearchStatus>;
severityList?: Array<AlarmSeverity>;
assignedToMe?: boolean;
}
@Component({ @Component({
selector: 'tb-alarm-table', selector: 'tb-alarm-table',
@ -59,7 +69,7 @@ export class AlarmTableComponent implements OnInit {
set entityId(entityId: EntityId) { set entityId(entityId: EntityId) {
this.entityIdValue = entityId; this.entityIdValue = entityId;
if (this.alarmTableConfig && this.alarmTableConfig.entityId !== entityId) { if (this.alarmTableConfig && this.alarmTableConfig.entityId !== entityId) {
this.alarmTableConfig.searchStatus = AlarmSearchStatus.ANY; this.alarmTableConfig.alarmFilterConfig = {statusList: [AlarmSearchStatus.ACTIVE]};
this.alarmTableConfig.entityId = entityId; this.alarmTableConfig.entityId = entityId;
this.entitiesTable.resetSortAndFilter(this.activeValue); this.entitiesTable.resetSortAndFilter(this.activeValue);
if (!this.activeValue) { if (!this.activeValue) {
@ -73,6 +83,7 @@ export class AlarmTableComponent implements OnInit {
alarmTableConfig: AlarmTableConfig; alarmTableConfig: AlarmTableConfig;
constructor(private alarmService: AlarmService, constructor(private alarmService: AlarmService,
private entityService: EntityService,
private dialogService: DialogService, private dialogService: DialogService,
private translate: TranslateService, private translate: TranslateService,
private datePipe: DatePipe, private datePipe: DatePipe,
@ -82,7 +93,8 @@ export class AlarmTableComponent implements OnInit {
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private utilsService: UtilsService, private utilsService: UtilsService,
private route: ActivatedRoute) { private route: ActivatedRoute,
private router: Router) {
} }
ngOnInit() { ngOnInit() {
@ -94,15 +106,52 @@ export class AlarmTableComponent implements OnInit {
if (isDefinedAndNotNull(this.route.snapshot.data.alarmsMode)) { if (isDefinedAndNotNull(this.route.snapshot.data.alarmsMode)) {
this.alarmsMode = this.route.snapshot.data.alarmsMode; this.alarmsMode = this.route.snapshot.data.alarmsMode;
} }
const defaultAlarmFilterConfig: AlarmFilterConfig = {statusList: [AlarmSearchStatus.ACTIVE]};
if (pageMode) {
const routerQueryParams: AlarmPageQueryParams = this.route.snapshot.queryParams;
if (routerQueryParams) {
const queryParams = deepClone(routerQueryParams);
let replaceUrl = false;
if (routerQueryParams?.typeList) {
defaultAlarmFilterConfig.typeList = routerQueryParams?.typeList;
delete queryParams.typeList;
replaceUrl = true;
}
if (routerQueryParams?.statusList) {
defaultAlarmFilterConfig.statusList = routerQueryParams?.statusList;
delete queryParams.statusList;
replaceUrl = true;
}
if (routerQueryParams?.severityList) {
defaultAlarmFilterConfig.severityList = routerQueryParams?.severityList;
delete queryParams.severityList;
replaceUrl = true;
}
if (routerQueryParams?.assignedToMe) {
defaultAlarmFilterConfig.assignedToCurrentUser = routerQueryParams?.assignedToMe;
delete queryParams.assignedToMe;
replaceUrl = true;
}
if (replaceUrl) {
this.router.navigate([], {
relativeTo: this.route,
queryParams,
queryParamsHandling: '',
replaceUrl: true
});
}
}
}
this.alarmTableConfig = new AlarmTableConfig( this.alarmTableConfig = new AlarmTableConfig(
this.alarmService, this.alarmService,
this.entityService,
this.dialogService, this.dialogService,
this.translate, this.translate,
this.datePipe, this.datePipe,
this.dialog, this.dialog,
this.alarmsMode, this.alarmsMode,
this.entityIdValue, this.entityIdValue,
AlarmSearchStatus.ANY, defaultAlarmFilterConfig,
this.store, this.store,
this.viewContainerRef, this.viewContainerRef,
this.overlay, this.overlay,

7
ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.ts

@ -21,12 +21,12 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { import {
AbstractControl, AbstractControl,
FormGroupDirective,
NgForm,
UntypedFormArray, UntypedFormArray,
UntypedFormBuilder, UntypedFormBuilder,
UntypedFormControl, UntypedFormControl,
UntypedFormGroup, UntypedFormGroup,
FormGroupDirective,
NgForm,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@ -107,7 +107,8 @@ export class EntityAliasesDialogComponent extends DialogComponent<EntityAliasesD
} else { } else {
const datasources = this.utils.validateDatasources(widget.config.datasources); const datasources = this.utils.validateDatasources(widget.config.datasources);
datasources.forEach((datasource) => { datasources.forEach((datasource) => {
if (datasource.type === DatasourceType.entity && datasource.entityAliasId) { if ([DatasourceType.entity, DatasourceType.entityCount, DatasourceType.alarmCount].includes(datasource.type)
&& datasource.entityAliasId) {
this.addWidgetTitleToWidgetsMap(datasource.entityAliasId, widget.config.title); this.addWidgetTitleToWidgetsMap(datasource.entityAliasId, widget.config.title);
} }
}); });

2
ui-ngx/src/app/modules/home/components/entity/entities-table.component.html

@ -42,7 +42,7 @@
<tb-anchor #entityTableHeader></tb-anchor> <tb-anchor #entityTableHeader></tb-anchor>
<tb-timewindow *ngIf="entitiesTableConfig.useTimePageLink" [(ngModel)]="timewindow" <tb-timewindow *ngIf="entitiesTableConfig.useTimePageLink" [(ngModel)]="timewindow"
(ngModelChange)="onTimewindowChange()" (ngModelChange)="onTimewindowChange()"
asButton historyOnly></tb-timewindow> asButton strokedButton historyOnly [forAllTimeEnabled]="entitiesTableConfig.forAllTimeEnabled"></tb-timewindow>
</div> </div>
<span fxFlex></span> <span fxFlex></span>
<div [fxShow]="addEnabled()"> <div [fxShow]="addEnabled()">

3
ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss

@ -69,6 +69,9 @@
.mat-toolbar { .mat-toolbar {
height: auto; height: auto;
min-height: 100px; min-height: 100px;
.mat-toolbar-tools{
height: auto;
}
.tb-entity-table-title{ .tb-entity-table-title{
padding-bottom: 5px; padding-bottom: 5px;

44
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -254,9 +254,9 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
if (this.entitiesTableConfig.useTimePageLink) { if (this.entitiesTableConfig.useTimePageLink) {
this.timewindow = this.entitiesTableConfig.defaultTimewindowInterval; this.timewindow = this.entitiesTableConfig.defaultTimewindowInterval;
const currentTime = Date.now(); const interval = this.getTimePageLinkInterval();
this.pageLink = new TimePageLink(10, 0, null, sortOrder, this.pageLink = new TimePageLink(10, 0, null, sortOrder,
currentTime - this.timewindow.history.timewindowMs, currentTime); interval.startTime, interval.endTime);
} else { } else {
this.pageLink = new PageLink(10, 0, null, sortOrder); this.pageLink = new PageLink(10, 0, null, sortOrder);
} }
@ -411,18 +411,9 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
} }
if (this.entitiesTableConfig.useTimePageLink) { if (this.entitiesTableConfig.useTimePageLink) {
const timePageLink = this.pageLink as TimePageLink; const timePageLink = this.pageLink as TimePageLink;
if (this.timewindow.history.historyType === HistoryWindowType.LAST_INTERVAL) { const interval = this.getTimePageLinkInterval();
const currentTime = Date.now(); timePageLink.startTime = interval.startTime;
timePageLink.startTime = currentTime - this.timewindow.history.timewindowMs; timePageLink.endTime = interval.endTime;
timePageLink.endTime = currentTime;
} else if (this.timewindow.history.historyType === HistoryWindowType.INTERVAL) {
const startEndTime = calculateIntervalStartEndTime(this.timewindow.history.quickInterval);
timePageLink.startTime = startEndTime[0];
timePageLink.endTime = startEndTime[1];
} else {
timePageLink.startTime = this.timewindow.history.fixedTimewindow.startTimeMs;
timePageLink.endTime = this.timewindow.history.fixedTimewindow.endTimeMs;
}
} }
this.dataSource.loadEntities(this.pageLink); this.dataSource.loadEntities(this.pageLink);
if (this.isDetailsOpen && this.entityDetailsPanel) { if (this.isDetailsOpen && this.entityDetailsPanel) {
@ -430,6 +421,31 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
} }
} }
private getTimePageLinkInterval(): {startTime?: number, endTime?: number} {
const interval: {startTime?: number, endTime?: number} = {};
switch (this.timewindow.history.historyType) {
case HistoryWindowType.LAST_INTERVAL:
const currentTime = Date.now();
interval.startTime = currentTime - this.timewindow.history.timewindowMs;
interval.endTime = currentTime;
break;
case HistoryWindowType.FIXED:
interval.startTime = this.timewindow.history.fixedTimewindow.startTimeMs;
interval.endTime = this.timewindow.history.fixedTimewindow.endTimeMs;
break;
case HistoryWindowType.INTERVAL:
const startEndTime = calculateIntervalStartEndTime(this.timewindow.history.quickInterval);
interval.startTime = startEndTime[0];
interval.endTime = startEndTime[1];
break;
case HistoryWindowType.FOR_ALL_TIME:
interval.startTime = null;
interval.endTime = null;
break;
}
return interval;
}
private dataLoaded(col?: number, row?: number) { private dataLoaded(col?: number, row?: number) {
if (isFinite(col) && isFinite(row)) { if (isFinite(col) && isFinite(row)) {
this.clearCellCache(col, row); this.clearCellCache(col, row);

8
ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html

@ -33,10 +33,10 @@
<span fxFlex="5"></span> <span fxFlex="5"></span>
<div fxFlex="95" fxLayout="row" fxLayoutAlign="start center"> <div fxFlex="95" fxLayout="row" fxLayoutAlign="start center">
<div class="tb-header-label" translate fxFlex>filter.filter</div> <div class="tb-header-label" translate fxFlex>filter.filter</div>
<div class="tb-header-label" translate fxFlex="120px" <div class="tb-header-label" translate fxFlex="80px"
fxLayout="column" fxLayoutAlign="center center" fxLayout="column" fxLayoutAlign="center center"
style="padding-left: 30px;">filter.editable</div> style="padding-left: 5px;">filter.editable</div>
<div style="min-width: 80px;"></div> <div style="min-width: 132px;"></div>
</div> </div>
</div> </div>
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">
@ -47,7 +47,7 @@
<span fxFlex="5">{{$index + 1}}.</span> <span fxFlex="5">{{$index + 1}}.</span>
<div class="mat-elevation-z4 tb-filter" fxFlex="95" fxLayout="row" fxLayoutAlign="start center"> <div class="mat-elevation-z4 tb-filter" fxFlex="95" fxLayout="row" fxLayoutAlign="start center">
<mat-label fxFlex>{{filterControl.get('filter').value}}</mat-label> <mat-label fxFlex>{{filterControl.get('filter').value}}</mat-label>
<section fxFlex="120px" style="padding-left: 10px;" <section fxFlex="80px" style="padding-left: 10px;"
class="tb-editable-switch" class="tb-editable-switch"
fxLayout="column" fxLayout="column"
fxLayoutAlign="center center"> fxLayoutAlign="center center">

9
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -178,6 +178,9 @@ import { modulesMap } from '@modules/common/modules-map';
import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component'; import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component';
import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { RouterTabsComponent } from '@home/components/router-tabs.component';
import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.component'; import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.component';
import { AlarmFilterConfigComponent } from '@home/components/alarm/alarm-filter-config.component';
import { AlarmAssigneeSelectPanelComponent } from '@home/components/alarm/alarm-assignee-select-panel.component';
import { AlarmAssigneeSelectComponent } from '@home/components/alarm/alarm-assignee-select.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -202,6 +205,9 @@ import { SendNotificationButtonComponent } from '@home/components/notification/s
AlarmTableHeaderComponent, AlarmTableHeaderComponent,
AlarmTableComponent, AlarmTableComponent,
AlarmAssigneePanelComponent, AlarmAssigneePanelComponent,
AlarmAssigneeSelectComponent,
AlarmAssigneeSelectPanelComponent,
AlarmFilterConfigComponent,
AttributeTableComponent, AttributeTableComponent,
AddAttributeDialogComponent, AddAttributeDialogComponent,
EditAttributeValuePanelComponent, EditAttributeValuePanelComponent,
@ -352,6 +358,9 @@ import { SendNotificationButtonComponent } from '@home/components/notification/s
RelationFiltersComponent, RelationFiltersComponent,
AlarmTableComponent, AlarmTableComponent,
AlarmAssigneePanelComponent, AlarmAssigneePanelComponent,
AlarmAssigneeSelectComponent,
AlarmAssigneeSelectPanelComponent,
AlarmFilterConfigComponent,
AttributeTableComponent, AttributeTableComponent,
AliasesEntitySelectComponent, AliasesEntitySelectComponent,
AliasesEntityAutocompleteComponent, AliasesEntityAutocompleteComponent,

1
ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts

@ -467,6 +467,7 @@ export class ImportExportService {
const ruleChainNode: RuleNode = { const ruleChainNode: RuleNode = {
name: '', name: '',
debugMode: false, debugMode: false,
singletonMode: false,
type: 'org.thingsboard.rule.engine.flow.TbRuleChainInputNode', type: 'org.thingsboard.rule.engine.flow.TbRuleChainInputNode',
configuration: { configuration: {
ruleChainId: ruleChainConnection.targetRuleChainId.id ruleChainId: ruleChainConnection.targetRuleChainId.id

3
ui-ngx/src/app/modules/home/components/notification/notification-bell.component.ts

@ -25,7 +25,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { NotificationWebsocketService } from '@core/ws/notification-websocket.service'; import { NotificationWebsocketService } from '@core/ws/notification-websocket.service';
import { BehaviorSubject, ReplaySubject, Subscription } from 'rxjs'; import { BehaviorSubject, ReplaySubject, Subscription } from 'rxjs';
import { distinctUntilChanged, share, tap } from 'rxjs/operators'; import { distinctUntilChanged, map, share, tap } from 'rxjs/operators';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { ShowNotificationPopoverComponent } from '@home/components/notification/show-notification-popover.component'; import { ShowNotificationPopoverComponent } from '@home/components/notification/show-notification-popover.component';
@ -47,6 +47,7 @@ export class NotificationBellComponent implements OnDestroy {
count$ = this.countSubject.asObservable().pipe( count$ = this.countSubject.asObservable().pipe(
distinctUntilChanged(), distinctUntilChanged(),
map((value) => value >= 100 ? '99+' : value),
tap(() => setTimeout(() => this.cd.markForCheck())), tap(() => setTimeout(() => this.cd.markForCheck())),
share({ share({
connector: () => new ReplaySubject(1) connector: () => new ReplaySubject(1)

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save