diff --git a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json index fc5b13154c..bfee44b341 100644 --- a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json @@ -2,8 +2,8 @@ "widgetsBundle": { "alias": "home_page_widgets", "title": "Home page widgets", - "image": null, - "description": null, + "image": "data:image/png;base64,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", + "description": "Useful to define home dashboard of the user. Contains widgets that enable navigation to necessary links.", "externalId": null, "name": "Home page widgets" }, @@ -11,8 +11,8 @@ { "alias": "documentation_links", "name": "Documentation links", - "image": null, - "description": null, + "image": "data:image/png;base64,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", + "description": "Display quick links to the platform or documentation.", "descriptor": { "type": "static", "sizeX": 7.5, @@ -30,8 +30,8 @@ { "alias": "getting_started", "name": "Getting started", - "image": null, - "description": null, + "image": "data:image/png;base64,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", + "description": "Gettings started instruction.", "descriptor": { "type": "static", "sizeX": 7.5, @@ -47,4 +47,4 @@ } } ] -} \ No newline at end of file +} diff --git a/application/src/main/data/upgrade/3.4.4/schema_update.sql b/application/src/main/data/upgrade/3.4.4/schema_update.sql index 3776eebc12..a248766c7e 100644 --- a/application/src/main/data/upgrade/3.4.4/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.4/schema_update.sql @@ -47,11 +47,15 @@ UPDATE alarm SET acknowledged = true, cleared = false WHERE status = 'ACTIVE_ACK UPDATE alarm SET acknowledged = false, cleared = true WHERE status = 'CLEARED_UNACK'; UPDATE alarm SET acknowledged = false, cleared = false WHERE status = 'ACTIVE_UNACK'; --- Drop index by 'status' column and replace with new one that has only active alarms; +-- Drop index by 'status' column and replace with new indexes that has only active alarms; DROP INDEX IF EXISTS idx_alarm_originator_alarm_type_active; CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type_active ON alarm USING btree (originator_id, type) WHERE cleared = false; +DROP INDEX IF EXISTS idx_alarm_tenant_alarm_type_active; +CREATE INDEX IF NOT EXISTS idx_alarm_tenant_alarm_type_active + ON alarm USING btree (tenant_id, type) WHERE cleared = false; + -- Cover index by alarm type to optimize propagated alarm queries; DROP INDEX IF EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id; CREATE INDEX IF NOT EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id ON entity_alarm @@ -610,3 +614,16 @@ 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'; + diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 26eab30d5d..515c635e68 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -87,9 +87,10 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; 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.TbServiceInfoProvider; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; +import org.thingsboard.server.queue.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.component.ComponentDiscoveryService; @@ -179,6 +180,10 @@ public class ActorSystemContext { @Setter private ComponentDiscoveryService componentService; + @Autowired + @Getter + private DiscoveryService discoveryService; + @Autowired @Getter private DataDecodingEncodingService encodingService; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java index a6049ce8f2..36e9113326 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java +++ b/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.RuleNodeException; 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.gen.transport.TransportProtos; /** * @author Andrew Shvayka @@ -39,11 +42,10 @@ import org.thingsboard.server.common.stats.TbApiUsageReportClient; public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor { private final String ruleChainName; - private final TbActorRef self; private final TbApiUsageReportClient apiUsageClient; + private final DefaultTbContext defaultCtx; private RuleNode ruleNode; private TbNode tbNode; - private DefaultTbContext defaultCtx; private RuleNodeInfo info; RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName, RuleNodeId ruleNodeId, ActorSystemContext systemContext @@ -51,7 +53,6 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor> void logEntityAction(SecurityUser user, EntityType entityType, E entity, E savedEntity, ActionType actionType, Exception e) { EntityId entityId = savedEntity != null ? savedEntity.getId() : emptyId(entityType); - entityActionService.logEntityAction(user, entityId, savedEntity != null ? savedEntity : entity, - user.getCustomerId(), actionType, e); + if (!user.isSystemAdmin()) { + entityActionService.logEntityAction(user, entityId, savedEntity != null ? savedEntity : entity, + user.getCustomerId(), actionType, e); + } } protected > E doSaveAndLog(EntityType entityType, E entity, BiFunction savingFunction) throws Exception { diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java index d39e200337..995ca30212 100644 --- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java @@ -41,7 +41,9 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger; import org.thingsboard.server.dao.audit.AuditLogService; +import org.thingsboard.server.queue.notification.NotificationRuleProcessor; import java.util.List; import java.util.Map; @@ -53,6 +55,7 @@ import java.util.stream.Collectors; public class EntityActionService { private final TbClusterService tbClusterService; private final AuditLogService auditLogService; + private final NotificationRuleProcessor notificationRuleProcessor; private static final ObjectMapper json = new ObjectMapper(); @@ -87,11 +90,11 @@ public class EntityActionService { case ALARM_CLEAR: msgType = DataConstants.ALARM_CLEAR; break; - case ALARM_ASSIGN: - msgType = DataConstants.ALARM_ASSIGN; + case ALARM_ASSIGNED: + msgType = DataConstants.ALARM_ASSIGNED; break; - case ALARM_UNASSIGN: - msgType = DataConstants.ALARM_UNASSIGN; + case ALARM_UNASSIGNED: + msgType = DataConstants.ALARM_UNASSIGNED; break; case ALARM_DELETE: msgType = DataConstants.ALARM_DELETE; @@ -187,6 +190,12 @@ public class EntityActionService { AlarmComment comment = extractParameter(AlarmComment.class, 0, additionalInfo); metaData.putValue("comment", json.writeValueAsString(comment)); } + if (actionType == ActionType.ADDED && !tenantId.isSysTenantId()) { + notificationRuleProcessor.process(EntitiesLimitTrigger.builder() + .tenantId(tenantId) + .entityType(entityId.getEntityType()) + .build()); + } ObjectNode entityNode; if (entity != null) { entityNode = json.valueToTree(entity); @@ -247,7 +256,7 @@ public class EntityActionService { } public void logEntityAction(User user, I entityId, E entity, CustomerId customerId, - ActionType actionType, Exception e, Object... additionalInfo) { + ActionType actionType, Exception e, Object... additionalInfo) { if (customerId == null || customerId.isNullUid()) { customerId = user.getCustomerId(); } diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index 0b3a4b8acd..9fdb1d0b8e 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -48,7 +48,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; +import org.thingsboard.server.queue.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java index 732d8ce409..b425694e81 100644 --- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java +++ b/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); scannedComponent.setName(ruleNodeAnnotation.name()); scannedComponent.setScope(ruleNodeAnnotation.scope()); + scannedComponent.setClusteringMode(ruleNodeAnnotation.clusteringMode()); NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation); ObjectNode configurationDescriptor = mapper.createObjectNode(); JsonNode node = mapper.valueToTree(nodeDefinition); diff --git a/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java index a0e6545652..4000e5a09d 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java @@ -90,35 +90,32 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService { @Override public ListenableFuture registerClaimingInfo(TenantId tenantId, DeviceId deviceId, String secretKey, long durationMs) { - ListenableFuture deviceFuture = deviceService.findDeviceByIdAsync(tenantId, deviceId); - return Futures.transformAsync(deviceFuture, device -> { - Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE); - List key = constructCacheKey(device.getId()); - - if (isAllowedClaimingByDefault) { - if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { - persistInCache(secretKey, durationMs, cache, key); - return Futures.immediateFuture(null); + Device device = deviceService.findDeviceById(tenantId, deviceId); + Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE); + List key = constructCacheKey(device.getId()); + if (isAllowedClaimingByDefault) { + if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + persistInCache(secretKey, durationMs, cache, key); + return Futures.immediateFuture(null); + } + log.warn("The device [{}] has been already claimed!", device.getName()); + return Futures.immediateFailedFuture(new IllegalArgumentException()); + } else { + ListenableFuture> claimingAllowedFuture = attributesService.find(tenantId, device.getId(), + DataConstants.SERVER_SCOPE, Collections.singletonList(CLAIM_ATTRIBUTE_NAME)); + return Futures.transform(claimingAllowedFuture, list -> { + if (list != null && !list.isEmpty()) { + Optional claimingAllowedOptional = list.get(0).getBooleanValue(); + if (claimingAllowedOptional.isPresent() && claimingAllowedOptional.get() + && device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + persistInCache(secretKey, durationMs, cache, key); + return null; + } } - log.warn("The device [{}] has been already claimed!", device.getName()); + log.warn("Failed to find claimingAllowed attribute for device or it is already claimed![{}]", device.getName()); throw new IllegalArgumentException(); - } else { - ListenableFuture> claimingAllowedFuture = attributesService.find(tenantId, device.getId(), - DataConstants.SERVER_SCOPE, Collections.singletonList(CLAIM_ATTRIBUTE_NAME)); - return Futures.transform(claimingAllowedFuture, list -> { - if (list != null && !list.isEmpty()) { - Optional claimingAllowedOptional = list.get(0).getBooleanValue(); - if (claimingAllowedOptional.isPresent() && claimingAllowedOptional.get() - && device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { - persistInCache(secretKey, durationMs, cache, key); - return null; - } - } - log.warn("Failed to find claimingAllowed attribute for device or it is already claimed![{}]", device.getName()); - throw new IllegalArgumentException(); - }, MoreExecutors.directExecutor()); - } - }, MoreExecutors.directExecutor()); + }, MoreExecutors.directExecutor()); + } } private ListenableFuture getClaimData(Cache cache, Device device) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index f273517b74..5171a6037f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -288,7 +288,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i if (session != null) { boolean success = false; if (session.isConnected()) { - session.startSyncProcess(tenantId, edgeId, true); + session.startSyncProcess(true); success = true; } clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success)); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 9e3a22f64f..33aed92682 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -148,7 +148,7 @@ public final class EdgeGrpcSession implements Closeable { if (requestMsg.getSyncRequestMsg().hasFullSync()) { fullSync = requestMsg.getSyncRequestMsg().getFullSync(); } - startSyncProcess(edge.getTenantId(), edge.getId(), fullSync); + startSyncProcess(fullSync); } else { syncCompleted = true; } @@ -192,10 +192,10 @@ public final class EdgeGrpcSession implements Closeable { }; } - public void startSyncProcess(TenantId tenantId, EdgeId edgeId, boolean fullSync) { - log.trace("[{}][{}] Staring edge sync process", tenantId, edgeId); + public void startSyncProcess(boolean fullSync) { + log.trace("[{}][{}][{}] Staring edge sync process", edge.getTenantId(), edge.getId(), this.sessionId); syncCompleted = false; - interruptGeneralProcessingOnSync(tenantId, edgeId); + interruptGeneralProcessingOnSync(); doSync(new EdgeSyncCursor(ctx, edge, fullSync)); } @@ -221,9 +221,9 @@ public final class EdgeGrpcSession implements Closeable { .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build()) .build(); - Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback() { + Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback<>() { @Override - public void onSuccess(Void result) { + public void onSuccess(Boolean isInterrupted) { syncCompleted = true; ctx.getClusterService().onEdgeEventUpdate(edge.getTenantId(), edge.getId()); } @@ -272,7 +272,7 @@ public final class EdgeGrpcSession implements Closeable { } if (sessionState.getPendingMsgsMap().isEmpty()) { log.debug("[{}] Pending msgs map is empty. Stopping current iteration", edge.getRoutingKey()); - stopCurrentSendDownlinkMsgsTask(null); + stopCurrentSendDownlinkMsgsTask(false); } } catch (Exception e) { log.error("[{}] Can't process downlink response message [{}]", this.sessionId, msg, e); @@ -367,14 +367,19 @@ public final class EdgeGrpcSession implements Closeable { if (isConnected() && !pageData.getData().isEmpty()) { log.trace("[{}] [{}] event(s) are going to be processed.", this.sessionId, pageData.getData().size()); List downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); - Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback() { + Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() { @Override - public void onSuccess(@Nullable Void tmp) { - if (isConnected() && pageData.hasNext()) { - processEdgeEvents(fetcher, pageLink.nextPageLink(), result); + public void onSuccess(@Nullable Boolean isInterrupted) { + if (Boolean.TRUE.equals(isInterrupted)) { + log.debug("[{}][{}][{}] Send downlink messages task was interrupted", edge.getTenantId(), edge.getId(), sessionId); + result.set(null); } else { - UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId(); - result.set(ifOffset); + if (isConnected() && pageData.hasNext()) { + processEdgeEvents(fetcher, pageLink.nextPageLink(), result); + } else { + UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId(); + result.set(ifOffset); + } } } @@ -394,7 +399,7 @@ public final class EdgeGrpcSession implements Closeable { } } - private ListenableFuture sendDownlinkMsgsPack(List downlinkMsgsPack) { + private ListenableFuture sendDownlinkMsgsPack(List downlinkMsgsPack) { interruptPreviousSendDownlinkMsgsTask(); sessionState.setSendDownlinkMsgsFuture(SettableFuture.create()); @@ -433,14 +438,14 @@ public final class EdgeGrpcSession implements Closeable { } else { log.warn("[{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy); - stopCurrentSendDownlinkMsgsTask(null); + stopCurrentSendDownlinkMsgsTask(false); } } else { - stopCurrentSendDownlinkMsgsTask(null); + stopCurrentSendDownlinkMsgsTask(false); } } catch (Exception e) { log.warn("[{}] Failed to send downlink msgs. Error msg {}", this.sessionId, e.getMessage(), e); - stopCurrentSendDownlinkMsgsTask(e); + stopCurrentSendDownlinkMsgsTask(true); } }; @@ -688,23 +693,18 @@ public final class EdgeGrpcSession implements Closeable { } private void interruptPreviousSendDownlinkMsgsTask() { - String msg = String.format("[%s] Previous send downlink future was not properly completed, stopping it now!", this.sessionId); - stopCurrentSendDownlinkMsgsTask(new RuntimeException(msg)); + log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId); + stopCurrentSendDownlinkMsgsTask(true); } - private void interruptGeneralProcessingOnSync(TenantId tenantId, EdgeId edgeId) { - String msg = String.format("[%s][%s] Sync process started. General processing interrupted!", tenantId, edgeId); - stopCurrentSendDownlinkMsgsTask(new RuntimeException(msg)); + private void interruptGeneralProcessingOnSync() { + log.debug("[{}][{}][{}] Sync process started. General processing interrupted!", edge.getTenantId(), edge.getId(), this.sessionId); + stopCurrentSendDownlinkMsgsTask(true); } - public void stopCurrentSendDownlinkMsgsTask(Exception e) { + public void stopCurrentSendDownlinkMsgsTask(Boolean isInterrupted) { if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()) { - if (e != null) { - log.debug(e.getMessage()); - sessionState.getSendDownlinkMsgsFuture().setException(e); - } else { - sessionState.getSendDownlinkMsgsFuture().set(null); - } + sessionState.getSendDownlinkMsgsFuture().set(isInterrupted); } if (sessionState.getScheduledSendDownlinkTask() != null) { sessionState.getScheduledSendDownlinkTask().cancel(true); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java index 59910c829f..09a7c5add8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java @@ -28,6 +28,6 @@ import java.util.concurrent.ScheduledFuture; public class EdgeSessionState { private final Map pendingMsgsMap = Collections.synchronizedMap(new LinkedHashMap<>()); - private SettableFuture sendDownlinkMsgsFuture; + private SettableFuture sendDownlinkMsgsFuture; private ScheduledFuture scheduledSendDownlinkTask; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java index 85eee6b2f2..742acdbcda 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java @@ -16,7 +16,6 @@ package org.thingsboard.server.service.edge.rpc.processor.device; import com.datastax.oss.driver.api.core.uuid.Uuids; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -108,8 +107,8 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor { public ListenableFuture processDeviceCredentialsMsg(TenantId tenantId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) { log.debug("[{}] Executing processDeviceCredentialsMsg, deviceCredentialsUpdateMsg [{}]", tenantId, deviceCredentialsUpdateMsg); DeviceId deviceId = new DeviceId(new UUID(deviceCredentialsUpdateMsg.getDeviceIdMSB(), deviceCredentialsUpdateMsg.getDeviceIdLSB())); - ListenableFuture deviceFuture = deviceService.findDeviceByIdAsync(tenantId, deviceId); - return Futures.transform(deviceFuture, device -> { + return dbCallbackExecutorService.submit(() -> { + Device device = deviceService.findDeviceById(tenantId, deviceId); if (device != null) { log.debug("Updating device credentials for device [{}]. New device credentials Id [{}], value [{}]", device.getName(), deviceCredentialsUpdateMsg.getCredentialsId(), deviceCredentialsUpdateMsg.getCredentialsValue()); @@ -129,6 +128,6 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor { log.warn("Can't find device by id [{}], deviceCredentialsUpdateMsg [{}]", deviceId, deviceCredentialsUpdateMsg); } return null; - }, dbCallbackExecutorService); + }); } } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index fe4f761f81..0a75404fb4 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -309,10 +309,10 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS return EdgeEventActionType.ALARM_ACK; case ALARM_CLEAR: return EdgeEventActionType.ALARM_CLEAR; - case ALARM_ASSIGN: - return EdgeEventActionType.ALARM_ASSIGN; - case ALARM_UNASSIGN: - return EdgeEventActionType.ALARM_UNASSIGN; + case ALARM_ASSIGNED: + return EdgeEventActionType.ALARM_ASSIGNED; + case ALARM_UNASSIGNED: + return EdgeEventActionType.ALARM_UNASSIGNED; case DELETED: return EdgeEventActionType.DELETED; case RELATION_ADD_OR_UPDATE: diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index 36e8c9f188..9f65756b30 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -175,7 +175,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } catch (ThingsboardException e) { log.error("Failed to save alarm comment", e); } - notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ASSIGN, user); + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ASSIGNED, user); } else { throw new ThingsboardException("Alarm was already assigned to this user!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } @@ -203,7 +203,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } catch (ThingsboardException e) { log.error("Failed to save alarm comment", e); } - notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGN, user); + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGNED, user); } else { throw new ThingsboardException("Alarm was already unassigned!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java index 8b3edbb812..57dba39063 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.notification.rule.NotificationRule; 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.plugin.ComponentLifecycleEvent; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; +import org.thingsboard.server.queue.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger; import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java index 54c53a54d2..7cf65bd132 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java @@ -38,7 +38,7 @@ public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificatio @Override public boolean matchesFilter(RuleEngineMsgTrigger trigger, AlarmAssignmentNotificationRuleTriggerConfig triggerConfig) { - Action action = trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGN) ? Action.ASSIGNED : Action.UNASSIGNED; + Action action = trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGNED) ? Action.ASSIGNED : Action.UNASSIGNED; if (!triggerConfig.getNotifyOn().contains(action)) { return false; } @@ -53,7 +53,7 @@ public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificatio AlarmInfo alarmInfo = JacksonUtil.fromString(trigger.getMsg().getData(), AlarmInfo.class); AlarmAssignee assignee = alarmInfo.getAssignee(); return AlarmAssignmentNotificationInfo.builder() - .action(trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGN) ? "assigned" : "unassigned") + .action(trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGNED) ? "assigned" : "unassigned") .assigneeFirstName(assignee != null ? assignee.getFirstName() : null) .assigneeLastName(assignee != null ? assignee.getLastName() : null) .assigneeEmail(assignee != null ? assignee.getEmail() : null) @@ -78,7 +78,7 @@ public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificatio @Override public Set getSupportedMsgTypes() { - return Set.of(DataConstants.ALARM_ASSIGN, DataConstants.ALARM_UNASSIGN); + return Set.of(DataConstants.ALARM_ASSIGNED, DataConstants.ALARM_UNASSIGNED); } } diff --git a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java index da2ec7042e..95ace87653 100644 --- a/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java @@ -21,7 +21,10 @@ import org.thingsboard.server.common.data.notification.info.EntitiesLimitNotific import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger; +import org.thingsboard.server.dao.entity.EntityCountService; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantService; import static org.apache.commons.collections.CollectionUtils.isNotEmpty; @@ -30,6 +33,8 @@ import static org.apache.commons.collections.CollectionUtils.isNotEmpty; @RequiredArgsConstructor public class EntitiesLimitTriggerProcessor implements NotificationRuleTriggerProcessor { + private final EntityCountService entityCountService; + private final TbTenantProfileCache tenantProfileCache; private final TenantService tenantService; @Override @@ -37,7 +42,15 @@ public class EntitiesLimitTriggerProcessor implements NotificationRuleTriggerPro if (isNotEmpty(triggerConfig.getEntityTypes()) && !triggerConfig.getEntityTypes().contains(trigger.getEntityType())) { return false; } - return (int) (trigger.getLimit() * triggerConfig.getThreshold()) == trigger.getCurrentCount(); // strict comparing not to send notification on each new entity + DefaultTenantProfileConfiguration profileConfiguration = tenantProfileCache.get(trigger.getTenantId()).getDefaultProfileConfiguration(); + long limit = profileConfiguration.getEntitiesLimit(trigger.getEntityType()); + if (limit <= 0) { + return false; + } + long currentCount = entityCountService.countByTenantIdAndEntityType(trigger.getTenantId(), trigger.getEntityType()); + trigger.setLimit(limit); + trigger.setCurrentCount(currentCount); + return (int) (limit * triggerConfig.getThreshold()) == currentCount; // strict comparing not to send notification on each new entity } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index dafbfc60d2..6a6612c581 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; +import org.thingsboard.server.queue.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java index 8e00c837f3..17af86dbf6 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java @@ -264,8 +264,7 @@ public class AccessValidator { if (currentUser.isSystemAdmin()) { callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); } else { - ListenableFuture deviceFuture = deviceService.findDeviceByIdAsync(currentUser.getTenantId(), new DeviceId(entityId.getId())); - Futures.addCallback(deviceFuture, getCallback(callback, device -> { + Futures.addCallback(Futures.immediateFuture(deviceService.findDeviceById(currentUser.getTenantId(), new DeviceId(entityId.getId()))), getCallback(callback, device -> { if (device == null) { return ValidationResult.entityNotFound(DEVICE_WITH_REQUESTED_ID_NOT_FOUND); } else { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java index b34ba3ef66..45805c5939 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java @@ -113,8 +113,9 @@ public abstract class AbstractBulkImportService importedEntityInfo = saveEntity(entityData.getFields(), user); E entity = importedEntityInfo.getEntity(); - saveKvs(user, entity, entityData.getKvs()); - + if (request.getMapping().getUpdate() || !importedEntityInfo.isUpdated()) { + saveKvs(user, entity, entityData.getKvs()); + } return importedEntityInfo; }, importedEntityInfo -> { diff --git a/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java b/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java index cce38cfd18..80d4cede03 100644 --- a/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java +++ b/application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.system; -import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.FutureCallback; import com.google.protobuf.ProtocolStringList; import lombok.RequiredArgsConstructor; @@ -23,10 +22,11 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.rule.engine.api.MailService; +import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.FeaturesInfo; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.SystemInfo; import org.thingsboard.server.common.data.SystemInfoData; import org.thingsboard.server.common.data.id.TenantId; @@ -38,7 +38,6 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.stats.TbApiUsageStateClient; import org.thingsboard.server.dao.oauth2.OAuth2Service; -import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo; import org.thingsboard.server.queue.discovery.DiscoveryService; @@ -59,10 +58,10 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import static org.thingsboard.common.util.SystemUtil.getCpuCount; import static org.thingsboard.common.util.SystemUtil.getCpuUsage; -import static org.thingsboard.common.util.SystemUtil.getMemoryUsage; import static org.thingsboard.common.util.SystemUtil.getDiscSpaceUsage; -import static org.thingsboard.common.util.SystemUtil.getCpuCount; +import static org.thingsboard.common.util.SystemUtil.getMemoryUsage; import static org.thingsboard.common.util.SystemUtil.getTotalDiscSpace; import static org.thingsboard.common.util.SystemUtil.getTotalMemory; @@ -90,6 +89,8 @@ public class DefaultSystemInfoService extends TbApplicationEventListener".equalsIgnoreCase(mailFrom.asText()); + try { + mailService.testConnection(TenantId.SYS_TENANT_ID); + return true; + } catch (Exception e) { + return false; + } + } + + private boolean isTwoFaEnabled() { + AdminSettings twoFaSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "twoFaSettings"); + if (twoFaSettings != null) { + var providers = twoFaSettings.getJsonValue().get("providers"); + if (providers != null) { + return providers.size() > 0; + } + } + return false; + } + + private boolean isSlackEnabled() { + AdminSettings notifications = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "notifications"); + if (notifications != null) { + return notifications.getJsonValue().get("deliveryMethodsConfigs").has("SLACK"); } return false; } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java index 07243711f3..2bb93d3dad 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java @@ -51,7 +51,7 @@ import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.alarm.AlarmOperationResult; import org.thingsboard.server.dao.alarm.AlarmService; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; +import org.thingsboard.server.queue.notification.NotificationRuleProcessor; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.entitiy.alarm.TbAlarmCommentService; import org.thingsboard.server.service.subscription.TbSubscriptionUtils; diff --git a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java index 0f80f0e3f6..612c1d6610 100644 --- a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java @@ -29,11 +29,10 @@ import org.springframework.web.client.RestTemplate; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.msg.notification.trigger.NewPlatformVersionTrigger; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; +import org.thingsboard.server.queue.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbCoreComponent; -import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.io.IOException; import java.nio.file.Files; diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 14bf47301b..1bc8c19a82 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1018,6 +1018,8 @@ queue: max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}" max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}" fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" + request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms + session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" confluent: ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" @@ -1036,11 +1038,12 @@ queue: # tb_rule_engine.sq: # - key: max.poll.records # value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" - other: # In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside - - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms - value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) - - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms - value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) + other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000" + other: # DEPRECATED. In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside + # - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms + # value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) + # - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms + # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java index 7cf6d7109a..930c4881f9 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java @@ -29,7 +29,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; @@ -180,7 +179,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), - tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED); alarm = updatedAlarm; alarm.setAssigneeId(null); @@ -192,7 +191,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), - tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGN); + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGNED); } @Test @@ -414,7 +413,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), - tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED); } @Test @@ -445,7 +444,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), - tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED); logout(); @@ -462,7 +461,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), - tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_ASSIGN); + tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_ASSIGNED); } @Test @@ -480,7 +479,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), - tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED); beforeAssignmentTs = System.currentTimeMillis(); Thread.sleep(2); @@ -491,7 +490,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), - tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGN); + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGNED); } @Test @@ -509,7 +508,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), - tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED); logout(); loginCustomerUser(); @@ -525,7 +524,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), - tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGN); + tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGNED); } @Test diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java index 9b05b38a22..6ffc216dd3 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.controller; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @@ -33,11 +34,13 @@ import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.ContextConfiguration; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest; @@ -68,6 +71,8 @@ import org.thingsboard.server.service.gateway_device.GatewayNotificationsService import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; @@ -1392,6 +1397,73 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { Assert.assertEquals(savedCredentials, updatedCredentials); } + @Test + public void testBulkImportDeviceWithUpdateFalse() throws Exception { + String deviceName = "firstDevice"; + String attributeValue = "testValue"; + BulkImportRequest request = new BulkImportRequest(); + request.setFile(String.format("NAME,TYPE,DATA\n%s,%s,%s", deviceName, "thermostat", attributeValue)); + BulkImportRequest.Mapping mapping = new BulkImportRequest.Mapping(); + BulkImportRequest.ColumnMapping name = new BulkImportRequest.ColumnMapping(); + name.setType(BulkImportColumnType.NAME); + BulkImportRequest.ColumnMapping type = new BulkImportRequest.ColumnMapping(); + type.setType(BulkImportColumnType.TYPE); + BulkImportRequest.ColumnMapping attribute = new BulkImportRequest.ColumnMapping(); + attribute.setType(BulkImportColumnType.SERVER_ATTRIBUTE); + attribute.setKey("DATA"); + List columns = new ArrayList<>(); + columns.add(name); + columns.add(type); + columns.add(attribute); + + mapping.setColumns(columns); + mapping.setDelimiter(','); + mapping.setUpdate(true); + mapping.setHeader(true); + request.setMapping(mapping); + + //import device + doPostWithTypedResponse("/api/device/bulk_import", request, new TypeReference<>() {}); + Device savedDevice = doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class); + + //check server attribute value + List> values = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + savedDevice.getId() + + "/values/attributes/SERVER_SCOPE", new TypeReference<>() { + }); + Map serverAttribute = values.stream().filter(att -> att.get("key").equals("DATA")).findFirst().get(); + Assert.assertEquals(attributeValue, serverAttribute.get("value")); + + //update server attribute value + String newAttributeValue = "testValue2"; + JsonNode content = JacksonUtil.toJsonNode("{\"DATA\": \"" + newAttributeValue + "\"}"); + doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + savedDevice.getUuidId() + "/SERVER_SCOPE", content) + .andExpect(status().isOk()); + + //reimport devices + String deviceName2 = "secondDevice"; + String attributeValue2 = "testValue3"; + + request.setFile(String.format("NAME,TYPE,DATA\n%s,%s,%s\n%s,%s,%s", deviceName, "thermostat", attributeValue, + deviceName2, "thermostat", attributeValue2)); + mapping.setUpdate(false); + doPostWithTypedResponse("/api/device/bulk_import", request, new TypeReference<>() {}); + Device savedDevice2 = doGet("/api/tenant/devices?deviceName=" + deviceName2, Device.class); + + //check attribute value was not changed after reimport + List> values2 = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + savedDevice.getId() + + "/values/attributes/SERVER_SCOPE", new TypeReference<>() { + }); + Map retrievedServerAttribute2 = values2.stream().filter(att -> att.get("key").equals("DATA")).findFirst().get(); + Assert.assertEquals(newAttributeValue, retrievedServerAttribute2.get("value")); + + //check attribute for second device + List> values3 = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + savedDevice2.getId() + + "/values/attributes/SERVER_SCOPE", new TypeReference<>() { + }); + Map retrievedServerAttribute3 = values3.stream().filter(att -> att.get("key").equals("DATA")).findFirst().get(); + Assert.assertEquals(attributeValue2, retrievedServerAttribute3.get("value")); + } + private Device createDevice(String name) { Device device = new Device(); device.setName(name); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java index 6dec1f0313..b71be1c3b6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java @@ -21,8 +21,12 @@ import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.MailService; +import org.thingsboard.rule.engine.api.SmsService; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.Customer; @@ -53,6 +57,7 @@ import org.thingsboard.server.common.data.query.TsValue; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.stats.TbApiUsageStateClient; +import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; @@ -76,6 +81,15 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest { @Autowired private TbTenantProfileCache tenantProfileCache; + @Autowired + private AdminSettingsService adminSettingsService; + + @MockBean + private MailService mailService; + + @MockBean + private SmsService smsService; + //For system administrator @Test public void testTenantsCountWsCmd() throws Exception { @@ -266,6 +280,19 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest { @Test public void testGetFeaturesInfo() throws Exception { + String mail = "test@thingsboard.org"; + Mockito.doAnswer(invocation -> { + AdminSettings mailSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"); + JsonNode mailFrom = mailSettings.getJsonValue().get("mailFrom"); + if (!mailFrom.asText().equals(mail)) { + throw new Exception(); + } + return null; + }).when(mailService).testConnection(TenantId.SYS_TENANT_ID); + + Mockito.when(smsService.isConfigured(TenantId.SYS_TENANT_ID)) + .then(a -> adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "sms") != null); + loginSysAdmin(); FeaturesInfo featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); @@ -279,7 +306,7 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest { AdminSettings mailSettings = doGet("/api/admin/settings/mail", AdminSettings.class); JsonNode jsonValue = mailSettings.getJsonValue(); - ((ObjectNode) jsonValue).put("mailFrom", "test@thingsboard.org"); + ((ObjectNode) jsonValue).put("mailFrom", mail); mailSettings.setJsonValue(jsonValue); doPost("/api/admin/settings", mailSettings).andExpect(status().isOk()); @@ -305,7 +332,12 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest { AdminSettings twoFaSettingsSettings = new AdminSettings(); twoFaSettingsSettings.setKey("twoFaSettings"); - twoFaSettingsSettings.setJsonValue(JacksonUtil.newObjectNode()); + + var twoFaSettings = JacksonUtil.newObjectNode(); + var providers = JacksonUtil.newArrayNode(); + providers.add(JacksonUtil.newObjectNode()); + twoFaSettings.set("providers", providers); + twoFaSettingsSettings.setJsonValue(twoFaSettings); doPost("/api/admin/settings", twoFaSettingsSettings).andExpect(status().isOk()); featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); @@ -317,7 +349,13 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest { AdminSettings notificationsSettings = new AdminSettings(); notificationsSettings.setKey("notifications"); - notificationsSettings.setJsonValue(JacksonUtil.newObjectNode()); + + var notificationSettings = JacksonUtil.newObjectNode(); + var deliveryMethodsConfigs = JacksonUtil.newObjectNode(); + deliveryMethodsConfigs.set("SLACK", JacksonUtil.newObjectNode()); + notificationSettings.set("deliveryMethodsConfigs", deliveryMethodsConfigs); + + notificationsSettings.setJsonValue(notificationSettings); doPost("/api/admin/settings", notificationsSettings).andExpect(status().isOk()); featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); @@ -338,6 +376,10 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest { Assert.assertTrue(featuresInfo.isTwoFaEnabled()); Assert.assertTrue(featuresInfo.isNotificationEnabled()); Assert.assertTrue(featuresInfo.isOauthEnabled()); + + adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "notifications"); + adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "twoFaSettings"); + adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "sms"); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java b/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java index 84d8a458d5..a1cc82fd5a 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java @@ -19,17 +19,14 @@ import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.notification.DefaultNotificationSettingsService; -import org.thingsboard.server.dao.notification.NotificationRuleService; -import org.thingsboard.server.dao.notification.NotificationTargetService; -import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.dao.settings.AdminSettingsService; @Service @Primary public class MockNotificationSettingsService extends DefaultNotificationSettingsService { - public MockNotificationSettingsService(AdminSettingsService adminSettingsService, NotificationTargetService notificationTargetService, NotificationTemplateService notificationTemplateService, NotificationRuleService notificationRuleService) { - super(adminSettingsService, notificationTargetService, notificationTemplateService, notificationRuleService); + public MockNotificationSettingsService(AdminSettingsService adminSettingsService) { + super(adminSettingsService, null, null); } @Override diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index 8fec807d45..6b21809874 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.BooleanNode; import org.junit.Before; import org.junit.Test; +import org.junit.function.ThrowingRunnable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.data.util.Pair; @@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.device.profile.AlarmCondition; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; @@ -41,6 +43,7 @@ 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.DeviceProfileAlarm; 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.notification.Notification; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; @@ -54,7 +57,9 @@ 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.AlarmNotificationRuleTriggerConfig; 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.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.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; @@ -63,24 +68,33 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; 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.TenantProfileData; +import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.notification.NotificationRequestService; +import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.service.apiusage.limits.LimitedApi; import org.thingsboard.server.service.apiusage.limits.RateLimitService; +import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.UUID; +import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.offset; @@ -98,7 +112,11 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { @Autowired private TenantProfileService tenantProfileService; @Autowired + private TbTenantProfileService tbTenantProfileService; + @Autowired private RateLimitService rateLimitService; + @Autowired + private RuleChainService ruleChainService; @Before public void beforeEach() throws Exception { @@ -107,56 +125,33 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { @Test public void testNotificationRuleProcessing_entityActionTrigger() throws Exception { - String notificationSubject = "${actionType}: ${entityType} [${entityId}]"; - String notificationText = "User: ${userEmail}"; - NotificationTemplate notificationTemplate = createNotificationTemplate(NotificationType.GENERAL, notificationSubject, notificationText, NotificationDeliveryMethod.WEB); - - NotificationRule notificationRule = new NotificationRule(); - notificationRule.setName("Web notification when any device is created, updated or deleted"); - notificationRule.setTemplateId(notificationTemplate.getId()); - notificationRule.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); - EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig(); triggerConfig.setEntityTypes(Set.of(EntityType.DEVICE)); triggerConfig.setCreated(true); triggerConfig.setUpdated(true); triggerConfig.setDeleted(true); + createNotificationRule(triggerConfig, "${actionType}: ${entityType} [${entityId}]", + "User: ${userEmail}", createNotificationTarget(tenantAdminUserId).getId()); + + Device device = checkNotificationAfter(() -> { + return createDevice("DEVICE!!!", "default", "12345"); + }, (notification, newDevice) -> { + assertThat(notification.getSubject()).isEqualTo("added: Device [" + newDevice.getId() + "]"); + assertThat(notification.getText()).isEqualTo("User: " + TENANT_ADMIN_EMAIL); + }); - DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); - recipientsConfig.setTargets(List.of(createNotificationTarget(tenantAdminUserId).getUuidId())); - - notificationRule.setTriggerConfig(triggerConfig); - notificationRule.setRecipientsConfig(recipientsConfig); - notificationRule = saveNotificationRule(notificationRule); - - getWsClient().subscribeForUnreadNotifications(10).waitForReply(true); - - - getWsClient().registerWaitForUpdate(); - Device device = createDevice("DEVICE!!!", "default", "12345"); - getWsClient().waitForUpdate(true); - - Notification notification = getWsClient().getLastDataUpdate().getUpdate(); - assertThat(notification.getSubject()).isEqualTo("added: Device [" + device.getId() + "]"); - assertThat(notification.getText()).isEqualTo("User: " + TENANT_ADMIN_EMAIL); - - - getWsClient().registerWaitForUpdate(); - device.setName("Updated name"); - device = doPost("/api/device", device, Device.class); - getWsClient().waitForUpdate(true); - - notification = getWsClient().getLastDataUpdate().getUpdate(); - assertThat(notification.getSubject()).isEqualTo("updated: Device [" + device.getId() + "]"); - - - getWsClient().registerWaitForUpdate(); - doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); - getWsClient().waitForUpdate(true); + checkNotificationAfter(() -> { + device.setName("Updated name"); + doPost("/api/device", device, Device.class); + }, notification -> { + assertThat(notification.getSubject()).isEqualTo("updated: Device [" + device.getId() + "]"); + }); - notification = getWsClient().getLastDataUpdate().getUpdate(); - assertThat(notification.getSubject()).isEqualTo("deleted: Device [" + device.getId() + "]"); + checkNotificationAfter(() -> { + doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); + }, notification -> { + assertThat(notification.getSubject()).isEqualTo("deleted: Device [" + device.getId() + "]"); + }); } @Test @@ -215,7 +210,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { clients.forEach((expectedDelay, wsClient) -> { Notification notification = wsClient.getLastDataUpdate().getUpdate(); double actualDelay = (double) (notification.getCreatedTime() - ts) / 1000; - assertThat(actualDelay).isCloseTo(expectedDelay, offset(0.5)); + assertThat(actualDelay).isCloseTo(expectedDelay, offset(2.0)); assertThat(notification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + AlarmStatus.ACTIVE_UNACK + ", " + "severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase() + ", deviceId: " + device.getId()); @@ -312,6 +307,79 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { assertThat(findNotificationRequests(EntityType.ALARM).getData()).filteredOn(NotificationRequest::isScheduled).isEmpty(); } + @Test + public void testNotificationRuleProcessing_entitiesLimit() throws Exception { + TenantProfile tenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID); + TenantProfileData profileData = tenantProfile.getProfileData(); + DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) profileData.getConfiguration(); + int limit = 5; + profileConfiguration.setMaxDevices(limit); + profileConfiguration.setMaxAssets(limit); + profileConfiguration.setMaxCustomers(limit); + profileConfiguration.setMaxUsers(limit); + profileConfiguration.setMaxDashboards(limit); + profileConfiguration.setMaxRuleChains(limit); + tenantProfile.setProfileData(profileData); + tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, null); + + EntitiesLimitNotificationRuleTriggerConfig triggerConfig = EntitiesLimitNotificationRuleTriggerConfig.builder() + .entityTypes(null).threshold(0.8f) + .build(); + loginSysAdmin(); + NotificationRule rule = createNotificationRule(triggerConfig, "${entityType}s limit will be reached soon", + "${entityType}s usage: ${currentCount}/${limit} (${percents}%)", createNotificationTarget(tenantAdminUserId).getId()); + int threshold = (int) (limit * 0.8); + loginTenantAdmin(); + + checkNotificationAfter(() -> { + for (int i = 1; i <= threshold; i++) { + createDevice(i + "", i + ""); + } + }, notification -> { + assertThat(notification.getText()).isEqualTo("Devices usage: " + threshold + "/" + limit + " (80%)"); + }); + + checkNotificationAfter(() -> { + for (int i = 1; i <= threshold; i++) { + Asset asset = new Asset(); + asset.setType("Test"); + asset.setName(i + ""); + doPost("/api/asset", asset); + } + }, notification -> { + assertThat(notification.getText()).isEqualTo("Assets usage: " + threshold + "/" + limit + " (80%)"); + }); + + checkNotificationAfter(() -> { + long present = ruleChainService.countByTenantId(tenantId); + for (int i = 1; i <= threshold - present; i++) { + RuleChain ruleChain = new RuleChain(); + ruleChain.setName(i + ""); + ruleChain.setRoot(false); + ruleChain.setDebugMode(false); + ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); + RuleChainMetaData metaData = new RuleChainMetaData(); + metaData.setRuleChainId(ruleChain.getId()); + metaData.setNodes(List.of()); + doPost("/api/ruleChain/metadata", metaData); + } + }, notification -> { + assertThat(notification.getText()).isEqualTo("Rule chains usage: " + threshold + "/" + limit + " (80%)"); + }); + + triggerConfig.setThreshold(1.0f); + rule.setTriggerConfig(triggerConfig); + loginSysAdmin(); + saveNotificationRule(rule); + loginTenantAdmin(); + + checkNotificationAfter(() -> { + createDevice(limit + "", limit + ""); + }, notification -> { + assertThat(notification.getText()).isEqualTo("Devices usage: " + limit + "/" + limit + " (100%)"); + }); + } + @Test public void testNotificationRuleInfo() throws Exception { NotificationDeliveryMethod[] deliveryMethods = {NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL}; @@ -345,9 +413,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) profileData.getConfiguration(); profileConfiguration.setTenantNotificationRequestsPerRuleRateLimit(notificationRequestsLimit + ":300"); tenantProfile.setProfileData(profileData); - loginSysAdmin(); - doPost("/api/tenantProfile", tenantProfile).andExpect(status().isOk()); - loginTenantAdmin(); + tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, null); NotificationRule rule = new NotificationRule(); rule.setName("Device created"); @@ -386,6 +452,45 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { assertThat(getMyNotifications(false, 100)).size().isEqualTo(notificationRequestsLimit); } + private R checkNotificationAfter(Callable action, BiConsumer check) throws Exception { + if (getWsClient().getLastDataUpdate() == null) { + getWsClient().subscribeForUnreadNotifications(10).waitForReply(true); + } + getWsClient().registerWaitForUpdate(); + R result = action.call(); + getWsClient().waitForUpdate(true); + check.accept(getWsClient().getLastDataUpdate().getUpdate(), result); + return result; + } + + private void checkNotificationAfter(ThrowingRunnable action, Consumer check) throws Exception { + checkNotificationAfter(() -> { + try { + action.run(); + return null; + } catch (Throwable e) { + throw new Exception(e); + } + }, (notification, r) -> check.accept(notification)); + } + + private 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); + } + private DeviceProfile createDeviceProfileWithAlarmRules(String alarmType) { DeviceProfile deviceProfile = createDeviceProfile("For notification rule test"); deviceProfile.setTenantId(tenantId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 9eb18c76c5..0f0cf0467f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -74,8 +74,8 @@ public class DataConstants { public static final String TIMESERIES_DELETED = "TIMESERIES_DELETED"; public static final String ALARM_ACK = "ALARM_ACK"; public static final String ALARM_CLEAR = "ALARM_CLEAR"; - public static final String ALARM_ASSIGN = "ALARM_ASSIGN"; - public static final String ALARM_UNASSIGN = "ALARM_UNASSIGN"; + public static final String ALARM_ASSIGNED = "ALARM_ASSIGNED"; + public static final String ALARM_UNASSIGNED = "ALARM_UNASSIGNED"; public static final String ALARM_DELETE = "ALARM_DELETE"; public static final String COMMENT_CREATED = "COMMENT_CREATED"; public static final String COMMENT_UPDATED = "COMMENT_UPDATED"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java index 274882d60e..3f4aa11c1f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java @@ -46,6 +46,10 @@ public class StringUtils { return source != null && !source.isEmpty() && !source.trim().isEmpty(); } + public static String notBlankOrDefault(String src, String def) { + return isNotBlank(src) ? src : def; + } + public static String removeStart(final String str, final String remove) { if (isEmpty(str) || isEmpty(remove)) { return str; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index b479f1a236..51f1e27d24 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -40,8 +40,8 @@ public enum ActionType { ALARM_ACK(false), ALARM_CLEAR(false), ALARM_DELETE(false), - ALARM_ASSIGN(false), - ALARM_UNASSIGN(false), + ALARM_ASSIGNED(false), + ALARM_UNASSIGNED(false), LOGIN(false), LOGOUT(false), LOCKOUT(false), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java index fa7c8df271..ccdee12a0d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java @@ -17,8 +17,10 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.validation.NoXss; +import java.util.Objects; import java.util.Set; @Data @@ -42,12 +44,19 @@ public class MqttDeviceProfileTransportConfiguration implements DeviceProfileTra } public TransportPayloadTypeConfiguration getTransportPayloadTypeConfiguration() { - if (transportPayloadTypeConfiguration != null) { - return transportPayloadTypeConfiguration; - } else { - return new JsonTransportPayloadConfiguration(); - } + return Objects.requireNonNullElseGet(transportPayloadTypeConfiguration, JsonTransportPayloadConfiguration::new); } + public String getDeviceTelemetryTopic() { + return StringUtils.notBlankOrDefault(deviceTelemetryTopic, MqttTopics.DEVICE_TELEMETRY_TOPIC); + } + + public String getDeviceAttributesTopic() { + return StringUtils.notBlankOrDefault(deviceAttributesTopic, MqttTopics.DEVICE_ATTRIBUTES_TOPIC); + } + + public String getDeviceAttributesSubscribeTopic() { + return StringUtils.notBlankOrDefault(deviceAttributesSubscribeTopic, MqttTopics.DEVICE_ATTRIBUTES_TOPIC); + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java index 9b813ae89c..4d81dc071c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java @@ -31,8 +31,8 @@ public enum EdgeEventActionType { RPC_CALL, ALARM_ACK, ALARM_CLEAR, - ALARM_ASSIGN, - ALARM_UNASSIGN, + ALARM_ASSIGNED, + ALARM_UNASSIGNED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, CREDENTIALS_REQUEST, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmAssignmentNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmAssignmentNotificationRuleTriggerConfig.java index 8bcb6387e2..59ad9e5316 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmAssignmentNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmAssignmentNotificationRuleTriggerConfig.java @@ -15,7 +15,10 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; @@ -23,6 +26,9 @@ import javax.validation.constraints.NotEmpty; import java.util.Set; @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class AlarmAssignmentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { private Set alarmTypes; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentNotificationRuleTriggerConfig.java index 237dce5282..c34127b611 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentNotificationRuleTriggerConfig.java @@ -15,13 +15,19 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import java.util.Set; @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class AlarmCommentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { private Set alarmTypes; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmNotificationRuleTriggerConfig.java index 4b17c1da8f..6118e9da00 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmNotificationRuleTriggerConfig.java @@ -15,7 +15,10 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; @@ -24,6 +27,9 @@ import java.io.Serializable; import java.util.Set; @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class AlarmNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { private Set alarmTypes; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ApiUsageLimitNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ApiUsageLimitNotificationRuleTriggerConfig.java index ca2fa01cae..d5ce9dff68 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ApiUsageLimitNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ApiUsageLimitNotificationRuleTriggerConfig.java @@ -15,13 +15,19 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.ApiFeature; import org.thingsboard.server.common.data.ApiUsageStateValue; import java.util.Set; @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class ApiUsageLimitNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { private Set apiFeatures; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/DeviceActivityNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/DeviceActivityNotificationRuleTriggerConfig.java index c55ebc2f6e..8deeada7c0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/DeviceActivityNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/DeviceActivityNotificationRuleTriggerConfig.java @@ -15,13 +15,19 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import javax.validation.constraints.NotEmpty; import java.util.Set; import java.util.UUID; @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class DeviceActivityNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { private Set devices; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntitiesLimitNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntitiesLimitNotificationRuleTriggerConfig.java index b799d4b81c..a40ddfb081 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntitiesLimitNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntitiesLimitNotificationRuleTriggerConfig.java @@ -15,13 +15,19 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.EntityType; import javax.validation.constraints.Max; import java.util.Set; @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class EntitiesLimitNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { private Set entityTypes; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntityActionNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntityActionNotificationRuleTriggerConfig.java index e597d390b8..3a627a9e56 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntityActionNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntityActionNotificationRuleTriggerConfig.java @@ -15,12 +15,18 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.EntityType; import java.util.Set; @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class EntityActionNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { private Set entityTypes; // maybe add name filter ? diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java index 130f6d4f61..c28e190cc9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java @@ -15,13 +15,19 @@ */ package org.thingsboard.server.common.data.notification.rule.trigger; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import java.util.Set; import java.util.UUID; @Data +@AllArgsConstructor +@NoArgsConstructor +@Builder public class RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { private Set ruleChains; // if empty - all rule chains diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentClusteringMode.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentClusteringMode.java new file mode 100644 index 0000000000..8ebe277e0f --- /dev/null +++ b/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 +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java index 288801b8d6..bb6235a989 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java @@ -36,15 +36,17 @@ public class ComponentDescriptor extends SearchTextBased @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") @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") - @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; - @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; - @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; @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; public ComponentDescriptor() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java index 32d396725b..e6041c94f7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java @@ -48,7 +48,9 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo impl private String name; @ApiModelProperty(position = 6, value = "Enable/disable debug. ", example = "false") 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; @JsonIgnore private byte[] configurationBytes; @@ -69,6 +71,7 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo impl this.type = ruleNode.getType(); this.name = ruleNode.getName(); this.debugMode = ruleNode.isDebugMode(); + this.singletonMode = ruleNode.isSingletonMode(); this.setConfiguration(ruleNode.getConfiguration()); this.externalId = ruleNode.getExternalId(); } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/EntitiesLimitTrigger.java b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/EntitiesLimitTrigger.java index ce6505fe13..e6ce143fd0 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/EntitiesLimitTrigger.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/EntitiesLimitTrigger.java @@ -28,8 +28,9 @@ public class EntitiesLimitTrigger implements NotificationRuleTrigger { private final TenantId tenantId; private final EntityType entityType; - private final long currentCount; - private final long limit; + + private long limit; + private long currentCount; @Override public NotificationRuleTriggerType getType() { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusQueueConfigs.java b/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusQueueConfigs.java index 6c56d55d36..5e404bc928 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusQueueConfigs.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusQueueConfigs.java @@ -19,10 +19,9 @@ import lombok.Getter; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.queue.util.PropertyUtils; import javax.annotation.PostConstruct; -import java.util.HashMap; import java.util.Map; @Component @@ -55,24 +54,12 @@ public class TbServiceBusQueueConfigs { @PostConstruct private void init() { - coreConfigs = getConfigs(coreProperties); - ruleEngineConfigs = getConfigs(ruleEngineProperties); - transportApiConfigs = getConfigs(transportApiProperties); - notificationsConfigs = getConfigs(notificationsProperties); - jsExecutorConfigs = getConfigs(jsExecutorProperties); - vcConfigs = getConfigs(vcProperties); + coreConfigs = PropertyUtils.getProps(coreProperties); + ruleEngineConfigs = PropertyUtils.getProps(ruleEngineProperties); + transportApiConfigs = PropertyUtils.getProps(transportApiProperties); + notificationsConfigs = PropertyUtils.getProps(notificationsProperties); + jsExecutorConfigs = PropertyUtils.getProps(jsExecutorProperties); + vcConfigs = PropertyUtils.getProps(vcProperties); } - private Map getConfigs(String properties) { - Map configs = new HashMap<>(); - if (StringUtils.isNotEmpty(properties)) { - for (String property : properties.split(";")) { - int delimiterPosition = property.indexOf(":"); - String key = property.substring(0, delimiterPosition); - String value = property.substring(delimiterPosition + 1); - configs.put(key, value); - } - } - return configs; - } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java index 5c63fe6e7d..7ed188491b 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.TbProperty; +import org.thingsboard.server.queue.util.PropertyUtils; import java.util.Collections; import java.util.List; @@ -53,34 +54,34 @@ public class TbKafkaSettings { @Value("${queue.kafka.ssl.enabled:false}") private boolean sslEnabled; - @Value("${queue.kafka.ssl.truststore.location}") + @Value("${queue.kafka.ssl.truststore.location:}") private String sslTruststoreLocation; - @Value("${queue.kafka.ssl.truststore.password}") + @Value("${queue.kafka.ssl.truststore.password:}") private String sslTruststorePassword; - @Value("${queue.kafka.ssl.keystore.location}") + @Value("${queue.kafka.ssl.keystore.location:}") private String sslKeystoreLocation; - @Value("${queue.kafka.ssl.keystore.password}") + @Value("${queue.kafka.ssl.keystore.password:}") private String sslKeystorePassword; - @Value("${queue.kafka.ssl.key.password}") + @Value("${queue.kafka.ssl.key.password:}") private String sslKeyPassword; - @Value("${queue.kafka.acks}") + @Value("${queue.kafka.acks:all}") private String acks; - @Value("${queue.kafka.retries}") + @Value("${queue.kafka.retries:1}") private int retries; @Value("${queue.kafka.compression.type:none}") private String compressionType; - @Value("${queue.kafka.batch.size}") + @Value("${queue.kafka.batch.size:16384}") private int batchSize; - @Value("${queue.kafka.linger.ms}") + @Value("${queue.kafka.linger.ms:1}") private long lingerMs; @Value("${queue.kafka.max.request.size:1048576}") @@ -89,10 +90,10 @@ public class TbKafkaSettings { @Value("${queue.kafka.max.in.flight.requests.per.connection:5}") private int maxInFlightRequestsPerConnection; - @Value("${queue.kafka.buffer.memory}") + @Value("${queue.kafka.buffer.memory:33554432}") private long bufferMemory; - @Value("${queue.kafka.replication_factor}") + @Value("${queue.kafka.replication_factor:1}") @Getter private short replicationFactor; @@ -108,21 +109,31 @@ public class TbKafkaSettings { @Value("${queue.kafka.fetch_max_bytes:134217728}") private int fetchMaxBytes; + @Value("${queue.kafka.request.timeout.ms:30000}") + private int requestTimeoutMs; + + @Value("${queue.kafka.session.timeout.ms:10000}") + private int sessionTimeoutMs; + @Value("${queue.kafka.use_confluent_cloud:false}") private boolean useConfluent; - @Value("${queue.kafka.confluent.ssl.algorithm}") + @Value("${queue.kafka.confluent.ssl.algorithm:}") private String sslAlgorithm; - @Value("${queue.kafka.confluent.sasl.mechanism}") + @Value("${queue.kafka.confluent.sasl.mechanism:}") private String saslMechanism; - @Value("${queue.kafka.confluent.sasl.config}") + @Value("${queue.kafka.confluent.sasl.config:}") private String saslConfig; - @Value("${queue.kafka.confluent.security.protocol}") + @Value("${queue.kafka.confluent.security.protocol:}") private String securityProtocol; + @Value("${queue.kafka.other-inline:}") + private String otherInline; + + @Deprecated @Setter private List other; @@ -134,8 +145,6 @@ public class TbKafkaSettings { props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, servers); props.put(AdminClientConfig.RETRIES_CONFIG, retries); - configureSSL(props); - return props; } @@ -147,8 +156,6 @@ public class TbKafkaSettings { props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, fetchMaxBytes); props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, maxPollIntervalMs); - configureSSL(props); - props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class); @@ -174,7 +181,7 @@ public class TbKafkaSettings { return props; } - private Properties toProps() { + Properties toProps() { Properties props = new Properties(); if (useConfluent) { @@ -184,6 +191,11 @@ public class TbKafkaSettings { props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, securityProtocol); } + props.put(CommonClientConfigs.REQUEST_TIMEOUT_MS_CONFIG, requestTimeoutMs); + props.put(CommonClientConfigs.SESSION_TIMEOUT_MS_CONFIG, sessionTimeoutMs); + + props.putAll(PropertyUtils.getProps(otherInline)); + if (other != null) { other.forEach(kv -> props.put(kv.getKey(), kv.getValue())); } @@ -193,7 +205,7 @@ public class TbKafkaSettings { return props; } - private void configureSSL(Properties props) { + void configureSSL(Properties props) { if (sslEnabled) { props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SSL"); props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, sslTruststoreLocation); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java index 64dd6f2720..9888066ee7 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java @@ -19,10 +19,9 @@ import lombok.Getter; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.queue.util.PropertyUtils; import javax.annotation.PostConstruct; -import java.util.HashMap; import java.util.Map; @Component @@ -66,29 +65,17 @@ public class TbKafkaTopicConfigs { @PostConstruct private void init() { - coreConfigs = getConfigs(coreProperties); - ruleEngineConfigs = getConfigs(ruleEngineProperties); - transportApiRequestConfigs = getConfigs(transportApiProperties); - transportApiResponseConfigs = getConfigs(transportApiProperties); + coreConfigs = PropertyUtils.getProps(coreProperties); + ruleEngineConfigs = PropertyUtils.getProps(ruleEngineProperties); + transportApiRequestConfigs = PropertyUtils.getProps(transportApiProperties); + transportApiResponseConfigs = PropertyUtils.getProps(transportApiProperties); transportApiResponseConfigs.put(NUM_PARTITIONS_SETTING, "1"); - notificationsConfigs = getConfigs(notificationsProperties); - jsExecutorRequestConfigs = getConfigs(jsExecutorProperties); - jsExecutorResponseConfigs = getConfigs(jsExecutorProperties); + notificationsConfigs = PropertyUtils.getProps(notificationsProperties); + jsExecutorRequestConfigs = PropertyUtils.getProps(jsExecutorProperties); + jsExecutorResponseConfigs = PropertyUtils.getProps(jsExecutorProperties); jsExecutorResponseConfigs.put(NUM_PARTITIONS_SETTING, "1"); - fwUpdatesConfigs = getConfigs(fwUpdatesProperties); - vcConfigs = getConfigs(vcProperties); + fwUpdatesConfigs = PropertyUtils.getProps(fwUpdatesProperties); + vcConfigs = PropertyUtils.getProps(vcProperties); } - private Map getConfigs(String properties) { - Map configs = new HashMap<>(); - if (StringUtils.isNotEmpty(properties)) { - for (String property : properties.split(";")) { - int delimiterPosition = property.indexOf(":"); - String key = property.substring(0, delimiterPosition); - String value = property.substring(delimiterPosition + 1); - configs.put(key, value); - } - } - return configs; - } } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/NotificationRuleProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/notification/NotificationRuleProcessor.java similarity index 93% rename from common/message/src/main/java/org/thingsboard/server/common/msg/notification/NotificationRuleProcessor.java rename to common/queue/src/main/java/org/thingsboard/server/queue/notification/NotificationRuleProcessor.java index 380773c1b0..1fa9d82863 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/notification/NotificationRuleProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/notification/NotificationRuleProcessor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.msg.notification; +package org.thingsboard.server.queue.notification; import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java b/common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java index 85598b7318..3c3988389f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java @@ -20,7 +20,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSubscriptionSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSubscriptionSettings.java index 0a65c7ca7d..3d7ab85ecc 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSubscriptionSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSubscriptionSettings.java @@ -19,10 +19,9 @@ import lombok.Getter; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.queue.util.PropertyUtils; import javax.annotation.PostConstruct; -import java.util.HashMap; import java.util.Map; @Component @@ -56,24 +55,12 @@ public class TbPubSubSubscriptionSettings { @PostConstruct private void init() { - coreSettings = getSettings(coreProperties); - ruleEngineSettings = getSettings(ruleEngineProperties); - transportApiSettings = getSettings(transportApiProperties); - notificationsSettings = getSettings(notificationsProperties); - jsExecutorSettings = getSettings(jsExecutorProperties); - vcSettings = getSettings(vcProperties); + coreSettings = PropertyUtils.getProps(coreProperties); + ruleEngineSettings = PropertyUtils.getProps(ruleEngineProperties); + transportApiSettings = PropertyUtils.getProps(transportApiProperties); + notificationsSettings = PropertyUtils.getProps(notificationsProperties); + jsExecutorSettings = PropertyUtils.getProps(jsExecutorProperties); + vcSettings = PropertyUtils.getProps(vcProperties); } - private Map getSettings(String properties) { - Map configs = new HashMap<>(); - if (StringUtils.isNotEmpty(properties)) { - for (String property : properties.split(";")) { - int delimiterPosition = property.indexOf(":"); - String key = property.substring(0, delimiterPosition); - String value = property.substring(delimiterPosition + 1); - configs.put(key, value); - } - } - return configs; - } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/util/PropertyUtils.java b/common/queue/src/main/java/org/thingsboard/server/queue/util/PropertyUtils.java new file mode 100644 index 0000000000..089d7f2219 --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/util/PropertyUtils.java @@ -0,0 +1,40 @@ +/** + * 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.queue.util; + +import org.thingsboard.server.common.data.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +public class PropertyUtils { + + public static Map getProps(String properties) { + Map configs = new HashMap<>(); + if (StringUtils.isNotEmpty(properties)) { + for (String property : properties.split(";")) { + if (StringUtils.isNotEmpty(property)) { + int delimiterPosition = property.indexOf(":"); + String key = property.substring(0, delimiterPosition); + String value = property.substring(delimiterPosition + 1); + configs.put(key, value); + } + } + } + return configs; + } + +} diff --git a/common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaSettingsTest.java b/common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaSettingsTest.java new file mode 100644 index 0000000000..7e8c2d319d --- /dev/null +++ b/common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaSettingsTest.java @@ -0,0 +1,83 @@ +/** + * 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.queue.kafka; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +import java.util.Properties; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; + +@SpringBootTest(classes = TbKafkaSettings.class) +@TestPropertySource(properties = { + "queue.type=kafka", + "queue.kafka.bootstrap.servers=localhost:9092", + "queue.kafka.other-inline=metrics.recording.level:INFO;metrics.sample.window.ms:30000", +}) +class TbKafkaSettingsTest { + + @Autowired + TbKafkaSettings settings; + + @BeforeEach + void beforeEach() { + settings = spy(settings); //SpyBean is not aware on @ConditionalOnProperty, that is why the traditional spy in use + } + + @Test + void givenToProps_whenConfigureSSL_thenVerifyOnce() { + Properties props = settings.toProps(); + + assertThat(props).as("TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS").containsEntry("request.timeout.ms", 30000); + assertThat(props).as("TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS").containsEntry("session.timeout.ms", 10000); + + //other-inline + assertThat(props).as("metrics.recording.level").containsEntry("metrics.recording.level", "INFO"); + assertThat(props).as("TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS").containsEntry("metrics.sample.window.ms", "30000"); + + Mockito.verify(settings).toProps(); + Mockito.verify(settings).configureSSL(any()); + } + + @Test + void givenToAdminProps_whenConfigureSSL_thenVerifyOnce() { + settings.toAdminProps(); + Mockito.verify(settings).toProps(); + Mockito.verify(settings).configureSSL(any()); + } + + @Test + void givenToConsumerProps_whenConfigureSSL_thenVerifyOnce() { + settings.toConsumerProps("main"); + Mockito.verify(settings).toProps(); + Mockito.verify(settings).configureSSL(any()); + } + + @Test + void givenTotoProducerProps_whenConfigureSSL_thenVerifyOnce() { + settings.toProducerProps(); + Mockito.verify(settings).toProps(); + Mockito.verify(settings).configureSSL(any()); + } + +} \ No newline at end of file diff --git a/common/queue/src/test/java/org/thingsboard/server/queue/util/PropertyUtilsTest.java b/common/queue/src/test/java/org/thingsboard/server/queue/util/PropertyUtilsTest.java new file mode 100644 index 0000000000..0484cb314f --- /dev/null +++ b/common/queue/src/test/java/org/thingsboard/server/queue/util/PropertyUtilsTest.java @@ -0,0 +1,62 @@ +/** + * 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.queue.util; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class PropertyUtilsTest { + + @Test + void givenNullOrEmpty_whenGetConfig_thenEmptyMap() { + assertThat(PropertyUtils.getProps(null)).as("null property").isEmpty(); + assertThat(PropertyUtils.getProps("")).as("empty property").isEmpty(); + assertThat(PropertyUtils.getProps(";")).as("ends with ;").isEmpty(); + } + + @Test + void givenKafkaOtherProperties_whenGetConfig_thenReturnMappedValues() { + assertThat(PropertyUtils.getProps("metrics.recording.level:INFO;metrics.sample.window.ms:30000")) + .as("two pairs") + .isEqualTo(Map.of( + "metrics.recording.level", "INFO", + "metrics.sample.window.ms", "30000" + )); + + assertThat(PropertyUtils.getProps("metrics.recording.level:INFO;metrics.sample.window.ms:30000" + ";")) + .as("two pairs ends with ;") + .isEqualTo(Map.of( + "metrics.recording.level", "INFO", + "metrics.sample.window.ms", "30000" + )); + } + + @Test + void givenKafkaTopicProperties_whenGetConfig_thenReturnMappedValues() { + assertThat(PropertyUtils.getProps("retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1")) + .isEqualTo(Map.of( + "retention.ms", "604800000", + "segment.bytes", "26214400", + "retention.bytes", "1048576000", + "partitions", "1", + "min.insync.replicas", "1" + )); + } + +} diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index fa75709d83..6c50561363 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -18,7 +18,9 @@ package org.thingsboard.script.api.tbel; import org.mvel2.ExecutionContext; import org.mvel2.ParserConfiguration; import org.mvel2.execution.ExecutionArrayList; +import org.mvel2.execution.ExecutionHashMap; import org.mvel2.util.MethodStub; +import org.thingsboard.server.common.data.StringUtils; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -27,8 +29,12 @@ import java.math.RoundingMode; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Base64; +import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.Set; public class TbUtils { @@ -89,6 +95,14 @@ public class TbUtils { byte[].class))); parserConfig.addImport("bytesToHex", new MethodStub(TbUtils.class.getMethod("bytesToHex", ExecutionArrayList.class))); + parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap", + ExecutionContext.class, Map.class))); + parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap", + ExecutionContext.class, Map.class, boolean.class))); + parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap", + ExecutionContext.class, Map.class, List.class))); + parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap", + ExecutionContext.class, Map.class, List.class, boolean.class))); } public static String btoa(String input) { @@ -225,7 +239,7 @@ public class TbUtils { } ExecutionArrayList data = new ExecutionArrayList<>(ctx); for (int i = 0; i < len; i += 2) { - data.add((byte)((Character.digit(hex.charAt(i), 16) << 4) + data.add((byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16))); } return data; @@ -316,4 +330,62 @@ public class TbUtils { } return value; } + + public static ExecutionHashMap toFlatMap(ExecutionContext ctx, Map json) { + return toFlatMap(ctx, json, new ArrayList<>(), true); + } + + public static ExecutionHashMap toFlatMap(ExecutionContext ctx, Map json, boolean pathInKey) { + return toFlatMap(ctx, json, new ArrayList<>(), pathInKey); + } + + public static ExecutionHashMap toFlatMap(ExecutionContext ctx, Map json, List excludeList) { + return toFlatMap(ctx, json, excludeList, true); + } + + public static ExecutionHashMap toFlatMap(ExecutionContext ctx, Map json, List excludeList, boolean pathInKey) { + ExecutionHashMap map = new ExecutionHashMap<>(16, ctx); + parseRecursive(json, map, excludeList, "", pathInKey); + return map; + } + + private static void parseRecursive(Object json, Map map, List excludeList, String path, boolean pathInKey) { + if (json instanceof Map.Entry) { + Map.Entry entry = (Map.Entry) json; + if (StringUtils.isNotBlank(path)) { + path += "."; + } + if (excludeList.contains(entry.getKey())) { + return; + } + path += entry.getKey(); + json = entry.getValue(); + } + if (json instanceof Set || json instanceof List) { + String arrayPath = path + "."; + Object[] collection = ((Collection) json).toArray(); + for (int index = 0; index < collection.length; index++) { + parseRecursive(collection[index], map, excludeList, arrayPath + index, pathInKey); + } + } else if (json instanceof Map) { + Map node = (Map) json; + for (Map.Entry entry : node.entrySet()) { + parseRecursive(entry, map, excludeList, path, pathInKey); + } + } else { + if (pathInKey) { + map.put(path, json); + } else { + String key = path.substring(path.lastIndexOf('.') + 1); + if (StringUtils.isNumeric(key)) { + int pos = path.length(); + for (int i = 0; i < 2; i++) { + pos = path.lastIndexOf('.', pos - 1); + } + key = path.substring(pos + 1); + } + map.put(key, json); + } + } + } } diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index f4cd610b72..82cd74ca30 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -15,15 +15,50 @@ */ package org.thingsboard.script.api.tbel; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; +import org.mvel2.ExecutionContext; +import org.mvel2.ParserContext; +import org.mvel2.SandboxedParserConfiguration; +import org.mvel2.execution.ExecutionArrayList; +import org.mvel2.execution.ExecutionHashMap; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Calendar; import java.util.List; +import java.util.Random; + +@Slf4j public class TbUtilsTest { + private ExecutionContext ctx; + + @Before + public void before() { + SandboxedParserConfiguration parserConfig = ParserContext.enableSandboxedMode(); + parserConfig.addImport("JSON", TbJson.class); + parserConfig.registerDataType("Date", TbDate.class, date -> 8L); + parserConfig.registerDataType("Random", Random.class, date -> 8L); + parserConfig.registerDataType("Calendar", Calendar.class, date -> 8L); + try { + TbUtils.register(parserConfig); + } catch (Exception e) { + log.error("Cannot register functions", e); + } + ctx = new ExecutionContext(parserConfig); + Assert.assertNotNull(ctx); + } + + @After + public void after() { + ctx.stop(); + } + @Test public void parseHexToInt() { Assert.assertEquals(0xAB, TbUtils.parseHexToInt("AB")); @@ -87,6 +122,71 @@ public class TbUtilsTest { Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 3, false)); } + @Test + public void toFlatMap() { + ExecutionHashMap inputMap = new ExecutionHashMap<>(16, ctx); + inputMap.put("name", "Alice"); + inputMap.put("age", 30); + inputMap.put("devices", new ExecutionArrayList<>(List.of( + new ExecutionHashMap<>(16, ctx) {{ + put("id", "dev001"); + put("type", "sensor"); + }}, + new ExecutionHashMap<>(16, ctx) {{ + put("id", "dev002"); + put("type", "actuator"); + }} + ), ctx)); + inputMap.put("settings", new ExecutionHashMap<>(16, ctx) {{ + put("notifications", true); + put("timezone", "UTC-5"); + put("params", new ExecutionHashMap<>(16, ctx) {{ + put("param1", "value1"); + put("param2", "value2"); + put("param3", new ExecutionHashMap<>(16, ctx) {{ + put("subParam1", "value1"); + put("subParam2", "value2"); + }}); + }}); + }}); + ExecutionArrayList excludeList = new ExecutionArrayList<>(ctx); + excludeList.addAll(List.of("age", "id", "param1", "subParam2")); + + ExecutionHashMap expectedMapWithPath = new ExecutionHashMap<>(16, ctx); + expectedMapWithPath.put("name", "Alice"); + expectedMapWithPath.put("devices.0.type", "sensor"); + expectedMapWithPath.put("devices.1.type", "actuator"); + expectedMapWithPath.put("settings.notifications", true); + expectedMapWithPath.put("settings.timezone", "UTC-5"); + expectedMapWithPath.put("settings.params.param2", "value2"); + expectedMapWithPath.put("settings.params.param3.subParam1", "value1"); + + ExecutionHashMap actualMapWithPaths = TbUtils.toFlatMap(ctx, inputMap, excludeList, true); + + Assert.assertEquals(expectedMapWithPath, actualMapWithPaths); + + ExecutionHashMap expectedMapWithoutPaths = new ExecutionHashMap<>(16, ctx); + expectedMapWithoutPaths.put("timezone", "UTC-5"); + expectedMapWithoutPaths.put("name", "Alice"); + expectedMapWithoutPaths.put("id", "dev002"); + expectedMapWithoutPaths.put("subParam2", "value2"); + expectedMapWithoutPaths.put("type", "actuator"); + expectedMapWithoutPaths.put("subParam1", "value1"); + expectedMapWithoutPaths.put("param1", "value1"); + expectedMapWithoutPaths.put("notifications", true); + expectedMapWithoutPaths.put("age", 30); + expectedMapWithoutPaths.put("param2", "value2"); + + ExecutionHashMap actualMapWithoutPaths = TbUtils.toFlatMap(ctx, inputMap, false); + + Assert.assertEquals(expectedMapWithoutPaths, actualMapWithoutPaths); + } + + + private static String keyToValue(String key, String extraSymbol) { + return key + "Value" + (extraSymbol == null ? "" : extraSymbol); + } + private static List toList(byte[] data) { List result = new ArrayList<>(data.length); for (Byte b : data) { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java index 9b1e3e664e..a2c4b9db72 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java @@ -221,7 +221,7 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler onDeviceConnectProto(SparkplugTopic topic) throws ThingsboardException { try { - String deviceType = this.gateway.getDeviceType() + "-node"; + String deviceType = this.gateway.getDeviceType() + " device"; return onDeviceConnect(topic.getNodeDeviceName(), deviceType); } catch (RuntimeException e) { log.error("Failed Sparkplug Device connect proto!", e); diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index 7aa785107e..08f196c9d7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -166,8 +166,8 @@ public class AuditLogServiceImpl implements AuditLogService { case UPDATED: case ALARM_ACK: case ALARM_CLEAR: - case ALARM_ASSIGN: - case ALARM_UNASSIGN: + case ALARM_ASSIGNED: + case ALARM_UNASSIGNED: case RELATIONS_DELETED: case ASSIGNED_TO_TENANT: if (entity != null) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index a32f93d801..b9bcd83e93 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.device; -import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -73,14 +72,11 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; -import javax.annotation.Nullable; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.UUID; -import java.util.stream.Collectors; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -508,27 +504,20 @@ public class DeviceServiceImpl extends AbstractCachedEntityService> findDevicesByQuery(TenantId tenantId, DeviceSearchQuery query) { ListenableFuture> relations = relationService.findByQuery(tenantId, query.toEntitySearchQuery()); - ListenableFuture> devices = Futures.transformAsync(relations, r -> { + return Futures.transform(relations, r -> { EntitySearchDirection direction = query.toEntitySearchQuery().getParameters().getDirection(); - List> futures = new ArrayList<>(); + List devices = new ArrayList<>(); for (EntityRelation relation : r) { EntityId entityId = direction == EntitySearchDirection.FROM ? relation.getTo() : relation.getFrom(); if (entityId.getEntityType() == EntityType.DEVICE) { - futures.add(findDeviceByIdAsync(tenantId, new DeviceId(entityId.getId()))); + Device device = findDeviceById(tenantId, new DeviceId(entityId.getId())); + if (query.getDeviceTypes().contains(device.getType())) { + devices.add(device); + } } } - return Futures.successfulAsList(futures); + return devices; }, MoreExecutors.directExecutor()); - - devices = Futures.transform(devices, new Function<>() { - @Nullable - @Override - public List apply(@Nullable List deviceList) { - return deviceList == null ? Collections.emptyList() : deviceList.stream().filter(device -> query.getDeviceTypes().contains(device.getType())).collect(Collectors.toList()); - } - }, MoreExecutors.directExecutor()); - - return devices; } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 6197d8f908..bcbed91d2a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/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_TYPE_PROPERTY = "type"; 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_CLASS_PROPERTY = "clazz"; 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 DEBUG_MODE = "debug_mode"; + public static final String SINGLETON_MODE = "singleton_mode"; /** * Cassandra rule chain constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java index 10e8bb59d5..f22ccffed9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java +++ b/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.plugin.ComponentDescriptor; 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.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; @@ -50,6 +51,10 @@ public class ComponentDescriptorEntity extends BaseSqlEntity implements SearchTex @Column(name = ModelConstants.DEBUG_MODE) private boolean debugMode; + @Column(name = ModelConstants.SINGLETON_MODE) + private boolean singletonMode; + @Column(name = ModelConstants.EXTERNAL_ID_PROPERTY) private UUID externalId; @@ -81,6 +84,7 @@ public class RuleNodeEntity extends BaseSqlEntity implements SearchTex this.type = ruleNode.getType(); this.name = ruleNode.getName(); this.debugMode = ruleNode.isDebugMode(); + this.singletonMode = ruleNode.isSingletonMode(); this.searchText = ruleNode.getName(); this.configuration = ruleNode.getConfiguration(); this.additionalInfo = ruleNode.getAdditionalInfo(); @@ -109,6 +113,7 @@ public class RuleNodeEntity extends BaseSqlEntity implements SearchTex ruleNode.setType(type); ruleNode.setName(name); ruleNode.setDebugMode(debugMode); + ruleNode.setSingletonMode(singletonMode); ruleNode.setConfiguration(configuration); ruleNode.setAdditionalInfo(additionalInfo); if (externalId != null) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java index 8319468c04..534e3136ca 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.notification; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.RequiredArgsConstructor; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; @@ -24,32 +23,8 @@ import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AdminSettings; -import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.CacheConstants; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; -import org.thingsboard.server.common.data.id.NotificationTargetId; -import org.thingsboard.server.common.data.id.NotificationTemplateId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; -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.EscalatedNotificationRuleRecipientsConfig; -import org.thingsboard.server.common.data.notification.rule.NotificationRule; -import org.thingsboard.server.common.data.notification.rule.NotificationRuleConfig; -import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentNotificationRuleTriggerConfig; -import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentNotificationRuleTriggerConfig; -import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig; -import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction; -import org.thingsboard.server.common.data.notification.rule.trigger.ApiUsageLimitNotificationRuleTriggerConfig; -import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig; -import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent; -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.NewPlatformVersionNotificationRuleTriggerConfig; -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.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; 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.platform.AffectedTenantAdministratorsFilter; @@ -60,20 +35,10 @@ import org.thingsboard.server.common.data.notification.targets.platform.Platform import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter; import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; -import org.thingsboard.server.common.data.notification.template.NotificationTemplate; -import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; -import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; -import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.dao.settings.AdminSettingsService; import java.util.Collections; -import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.Set; - -import static org.thingsboard.common.util.JacksonUtil.newObjectNode; -import static org.thingsboard.server.dao.DaoUtil.toUUIDs; @Service @RequiredArgsConstructor @@ -81,8 +46,7 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS private final AdminSettingsService adminSettingsService; private final NotificationTargetService notificationTargetService; - private final NotificationTemplateService notificationTemplateService; - private final NotificationRuleService notificationRuleService; + private final DefaultNotifications defaultNotifications; private static final String SETTINGS_KEY = "notifications"; @@ -120,39 +84,21 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS NotificationTarget tenantAdmins = createTarget(tenantId, "Tenant administrators", new TenantAdministratorsFilter(), tenantId.isSysTenantId() ? "All tenant administrators" : "Tenant administrators"); - createTemplate(tenantId, "Maintenance work notification", NotificationType.GENERAL, - "Infrastructure maintenance", - "Maintenance work is scheduled for tomorrow (7:00 a.m. - 9:00 a.m. UTC)"); + defaultNotifications.create(tenantId, DefaultNotifications.maintenanceWork); if (tenantId.isSysTenantId()) { NotificationTarget sysAdmins = createTarget(tenantId, "System administrators", new SystemAdministratorsFilter(), "All system administrators"); NotificationTarget affectedTenantAdmins = createTarget(tenantId, "Affected tenant's administrators", new AffectedTenantAdministratorsFilter(), ""); - NotificationTemplate entitiesLimitNotificationTemplate = createTemplate(tenantId, "Entities count limit notification", NotificationType.ENTITIES_LIMIT, - "${entityType}s limit will be reached soon for tenant ${tenantName}", - "${entityType}s usage: ${currentCount}/${limit} (${percents}%)"); - EntitiesLimitNotificationRuleTriggerConfig entitiesLimitRuleTriggerConfig = new EntitiesLimitNotificationRuleTriggerConfig(); - entitiesLimitRuleTriggerConfig.setEntityTypes(null); - entitiesLimitRuleTriggerConfig.setThreshold(0.8f); - createRule(tenantId, "Entities count limit", entitiesLimitNotificationTemplate.getId(), entitiesLimitRuleTriggerConfig, - List.of(affectedTenantAdmins.getId(), sysAdmins.getId()), "Send notification to tenant admins and system admins when count of entities of some type reached 80% threshold of the limit"); + defaultNotifications.create(tenantId, DefaultNotifications.entitiesLimitForSysadmin, sysAdmins.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.entitiesLimitForTenant, affectedTenantAdmins.getId()); - NotificationTemplate apiUsageLimitNotificationTemplate = createTemplate(tenantId, "API usage limit notification", NotificationType.API_USAGE_LIMIT, - "${feature} feature - ${status:upperCase}", - "Tenant '${tenantName}': usage - ${currentValue} out of ${limit} ${unitLabel}s"); - ApiUsageLimitNotificationRuleTriggerConfig apiUsageLimitRuleTriggerConfig = new ApiUsageLimitNotificationRuleTriggerConfig(); - apiUsageLimitRuleTriggerConfig.setApiFeatures(null); - apiUsageLimitRuleTriggerConfig.setNotifyOn(Set.of(ApiUsageStateValue.WARNING, ApiUsageStateValue.DISABLED)); - createRule(tenantId, "API usage limit", apiUsageLimitNotificationTemplate.getId(), apiUsageLimitRuleTriggerConfig, - List.of(affectedTenantAdmins.getId(), sysAdmins.getId()), "Send notification to tenant admins and system admins when API feature usage state changed"); + defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureWarningForSysadmin, sysAdmins.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureWarningForTenant, affectedTenantAdmins.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureDisabledForSysadmin, sysAdmins.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.apiFeatureDisabledForTenant, affectedTenantAdmins.getId()); - NotificationTemplate newPlatformVersionNotificationTemplate = createTemplate(tenantId, "New platform version notification", NotificationType.NEW_PLATFORM_VERSION, - "New version ${latestVersion} is available", - "Current platform version is ${currentVersion}", - null, "Open release notes", "${latestVersionReleaseNotesUrl}"); - NewPlatformVersionNotificationRuleTriggerConfig newPlatformVersionRuleTriggerConfig = new NewPlatformVersionNotificationRuleTriggerConfig(); - createRule(tenantId, "New platform version", newPlatformVersionNotificationTemplate.getId(), newPlatformVersionRuleTriggerConfig, - List.of(sysAdmins.getId(), tenantAdmins.getId()), "Send notification to system admins and tenant admins when new platform version is available"); + defaultNotifications.create(tenantId, DefaultNotifications.newPlatformVersion, sysAdmins.getId(), tenantAdmins.getId()); return; } @@ -161,90 +107,13 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS NotificationTarget affectedUser = createTarget(tenantId, "Affected user", new AffectedUserFilter(), "If rule trigger is an action that affects some user (e.g. alarm assigned to user) - this user"); - NotificationTemplate newAlarmNotificationTemplate = createTemplate(tenantId, "New alarm notification", NotificationType.ALARM, - "New alarm '${alarmType}'", - "Severity: ${alarmSeverity}, originator: ${alarmOriginatorEntityType} '${alarmOriginatorName}'", - "notifications", null, null); - AlarmNotificationRuleTriggerConfig newAlarmRuleTriggerConfig = new AlarmNotificationRuleTriggerConfig(); - newAlarmRuleTriggerConfig.setAlarmTypes(null); - newAlarmRuleTriggerConfig.setAlarmSeverities(null); - newAlarmRuleTriggerConfig.setNotifyOn(Set.of(AlarmAction.CREATED)); - createRule(tenantId, "New alarm", newAlarmNotificationTemplate.getId(), newAlarmRuleTriggerConfig, - List.of(tenantAdmins.getId()), "Send notification to tenant admins when an alarm is created"); - - NotificationTemplate alarmUpdateNotificationTemplate = createTemplate(tenantId, "Alarm update notification", NotificationType.ALARM, - "Alarm '${alarmType}' - ${action}", - "Severity: ${alarmSeverity}, originator: ${alarmOriginatorEntityType} '${alarmOriginatorName}'", - "notifications", null, null); - AlarmNotificationRuleTriggerConfig alarmRuleTriggerConfig = new AlarmNotificationRuleTriggerConfig(); - alarmRuleTriggerConfig.setAlarmTypes(null); - alarmRuleTriggerConfig.setAlarmSeverities(null); - alarmRuleTriggerConfig.setNotifyOn(Set.of(AlarmAction.SEVERITY_CHANGED, AlarmAction.ACKNOWLEDGED, AlarmAction.CLEARED)); - createRule(tenantId, "Alarm update", alarmUpdateNotificationTemplate.getId(), alarmRuleTriggerConfig, - List.of(tenantAdmins.getId()), "Send notification to tenant admins when any alarm is updated or cleared"); - - NotificationTemplate deviceActionNotificationTemplate = createTemplate(tenantId, "Device action notification", NotificationType.ENTITY_ACTION, - "${entityType} was ${actionType}", - "${entityType} '${entityName}' was ${actionType} by user ${userEmail}", - "info", "Go to device", "/devices/${entityId}"); - EntityActionNotificationRuleTriggerConfig deviceActionRuleTriggerConfig = new EntityActionNotificationRuleTriggerConfig(); - deviceActionRuleTriggerConfig.setEntityTypes(Set.of(EntityType.DEVICE)); - deviceActionRuleTriggerConfig.setCreated(true); - deviceActionRuleTriggerConfig.setUpdated(false); - deviceActionRuleTriggerConfig.setDeleted(false); - createRule(tenantId, "Device created", deviceActionNotificationTemplate.getId(), deviceActionRuleTriggerConfig, - List.of(tenantAdmins.getId()), "Send notification to tenant admins when device is created"); - - NotificationTemplate deviceActivityNotificationTemplate = createTemplate(tenantId, "Device activity notification", NotificationType.DEVICE_ACTIVITY, - "Device '${deviceName}' became ${eventType}", - "Device '${deviceName}' of type '${deviceType}' is now ${eventType}", - "info", "Go to device", "/devices/${deviceId}"); - DeviceActivityNotificationRuleTriggerConfig deviceActivityRuleTriggerConfig = new DeviceActivityNotificationRuleTriggerConfig(); - deviceActivityRuleTriggerConfig.setDevices(null); - deviceActivityRuleTriggerConfig.setDeviceProfiles(null); - deviceActivityRuleTriggerConfig.setNotifyOn(Set.of(DeviceEvent.ACTIVE, DeviceEvent.INACTIVE)); - createRule(tenantId, "Device activity status change", deviceActivityNotificationTemplate.getId(), deviceActivityRuleTriggerConfig, - List.of(tenantAdmins.getId()), "Send notification to tenant admins when any device changes its activity state"); - - NotificationTemplate alarmCommentNotificationTemplate = createTemplate(tenantId, "Alarm comment notification", NotificationType.ALARM_COMMENT, - "Comment on '${alarmType}' alarm", - "${userEmail} ${action} comment: ${comment}", - "people", null, null); - AlarmCommentNotificationRuleTriggerConfig alarmCommentRuleTriggerConfig = new AlarmCommentNotificationRuleTriggerConfig(); - alarmCommentRuleTriggerConfig.setAlarmTypes(null); - alarmCommentRuleTriggerConfig.setAlarmSeverities(null); - alarmCommentRuleTriggerConfig.setAlarmStatuses(Set.of(AlarmSearchStatus.ACTIVE)); - alarmCommentRuleTriggerConfig.setOnlyUserComments(true); - alarmCommentRuleTriggerConfig.setNotifyOnCommentUpdate(false); - createRule(tenantId, "Comment on active alarm", alarmCommentNotificationTemplate.getId(), alarmCommentRuleTriggerConfig, - List.of(tenantAdmins.getId()), "Send notification to tenant admins when comment is added by user on active alarm"); - - NotificationTemplate alarmAssignedNotificationTemplate = createTemplate(tenantId, "Alarm assigned notification", NotificationType.ALARM_ASSIGNMENT, - "Alarm '${alarmType}' (${alarmSeverity}) was assigned to user", - "${userEmail} assigned alarm on ${alarmOriginatorEntityType} '${alarmOriginatorName}' to ${assigneeEmail}", - "person", null, null); - AlarmAssignmentNotificationRuleTriggerConfig alarmAssignmentRuleTriggerConfig = new AlarmAssignmentNotificationRuleTriggerConfig(); - alarmAssignmentRuleTriggerConfig.setAlarmTypes(null); - alarmAssignmentRuleTriggerConfig.setAlarmSeverities(null); - alarmAssignmentRuleTriggerConfig.setAlarmStatuses(null); - alarmAssignmentRuleTriggerConfig.setNotifyOn(Set.of(AlarmAssignmentNotificationRuleTriggerConfig.Action.ASSIGNED)); - createRule(tenantId, "Alarm assignment", alarmAssignedNotificationTemplate.getId(), alarmAssignmentRuleTriggerConfig, - List.of(affectedUser.getId()), "Send notification to user when any alarm was assigned to him"); - - NotificationTemplate ruleEngineComponentLifecycleFailureNotificationTemplate = createTemplate(tenantId, "Rule chain/node lifecycle failure notification", NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, - "${action:capitalize} failure in Rule chain '${ruleChainName}'", - "${componentType} '${componentName}' failed to ${action}", - "warning", "Go to rule chain", "/ruleChains/${ruleChainId}"); - RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig ruleEngineComponentLifecycleEventRuleTriggerConfig = new RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig(); - ruleEngineComponentLifecycleEventRuleTriggerConfig.setRuleChains(null); - ruleEngineComponentLifecycleEventRuleTriggerConfig.setRuleChainEvents(Set.of(ComponentLifecycleEvent.STARTED, ComponentLifecycleEvent.UPDATED, ComponentLifecycleEvent.STOPPED)); - ruleEngineComponentLifecycleEventRuleTriggerConfig.setOnlyRuleChainLifecycleFailures(true); - ruleEngineComponentLifecycleEventRuleTriggerConfig.setTrackRuleNodeEvents(true); - ruleEngineComponentLifecycleEventRuleTriggerConfig.setRuleNodeEvents(Set.of(ComponentLifecycleEvent.STARTED, ComponentLifecycleEvent.UPDATED, ComponentLifecycleEvent.STOPPED)); - ruleEngineComponentLifecycleEventRuleTriggerConfig.setOnlyRuleNodeLifecycleFailures(true); - createRule(tenantId, "Rule node initialization failure", ruleEngineComponentLifecycleFailureNotificationTemplate.getId(), - ruleEngineComponentLifecycleEventRuleTriggerConfig, List.of(tenantAdmins.getId()), - "Send notification to tenant admins when any Rule chain or Rule node failed to start, update or stop"); + defaultNotifications.create(tenantId, DefaultNotifications.newAlarm, tenantAdmins.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.alarmUpdate, tenantAdmins.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.deviceAction, tenantAdmins.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.deviceActivity, tenantAdmins.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.alarmComment, tenantAdmins.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.alarmAssignment, affectedUser.getId()); + defaultNotifications.create(tenantId, DefaultNotifications.ruleEngineComponentLifecycleFailure, tenantAdmins.getId()); } private NotificationTarget createTarget(TenantId tenantId, String name, UsersFilter filter, String description) { @@ -259,76 +128,4 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS return notificationTargetService.saveNotificationTarget(tenantId, target); } - private NotificationTemplate createTemplate(TenantId tenantId, String name, NotificationType notificationType, - String subjectTemplate, String textTemplate) { - return createTemplate(tenantId, name, notificationType, subjectTemplate, textTemplate, null, null, null); - } - - private NotificationTemplate createTemplate(TenantId tenantId, String name, NotificationType notificationType, - String subjectTemplate, String textTemplate, - String icon, String button, String link) { - NotificationTemplate template = new NotificationTemplate(); - template.setTenantId(tenantId); - template.setName(name); - template.setNotificationType(notificationType); - - NotificationTemplateConfig templateConfig = new NotificationTemplateConfig(); - WebDeliveryMethodNotificationTemplate webTemplate = new WebDeliveryMethodNotificationTemplate(); - webTemplate.setSubject(subjectTemplate); - webTemplate.setBody(textTemplate); - ObjectNode additionalConfig = newObjectNode(); - ObjectNode iconConfig = newObjectNode(); - additionalConfig.set("icon", iconConfig); - ObjectNode buttonConfig = newObjectNode(); - additionalConfig.set("actionButtonConfig", buttonConfig); - if (icon != null) { - iconConfig.put("enabled", true) - .put("icon", icon) - .put("color", "#757575"); - } else { - iconConfig.put("enabled", false); - } - if (button != null) { - buttonConfig.put("enabled", true) - .put("text", button) - .put("linkType", "LINK") - .put("link", link); - } else { - buttonConfig.put("enabled", false); - } - webTemplate.setAdditionalConfig(additionalConfig); - webTemplate.setEnabled(true); - templateConfig.setDeliveryMethodsTemplates(Map.of( - NotificationDeliveryMethod.WEB, webTemplate - )); - template.setConfiguration(templateConfig); - return notificationTemplateService.saveNotificationTemplate(tenantId, template); - } - - private NotificationRule createRule(TenantId tenantId, String name, NotificationTemplateId templateId, - NotificationRuleTriggerConfig triggerConfig, List targets, - String description) { - NotificationRule rule = new NotificationRule(); - rule.setTenantId(tenantId); - rule.setName(name); - rule.setTemplateId(templateId); - rule.setTriggerType(triggerConfig.getTriggerType()); - rule.setTriggerConfig(triggerConfig); - if (rule.getTriggerType() == NotificationRuleTriggerType.ALARM) { - EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(rule.getTriggerType()); - recipientsConfig.setEscalationTable(Map.of(0, toUUIDs(targets))); - rule.setRecipientsConfig(recipientsConfig); - } else { - DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); - recipientsConfig.setTriggerType(rule.getTriggerType()); - recipientsConfig.setTargets(toUUIDs(targets)); - rule.setRecipientsConfig(recipientsConfig); - } - NotificationRuleConfig additionalConfig = new NotificationRuleConfig(); - additionalConfig.setDescription(description); - rule.setAdditionalConfig(additionalConfig); - return notificationRuleService.saveNotificationRule(tenantId, rule); - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java new file mode 100644 index 0000000000..038ffe342a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java @@ -0,0 +1,375 @@ +/** + * 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.dao.notification; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; +import org.thingsboard.server.common.data.id.NotificationTargetId; +import org.thingsboard.server.common.data.id.NotificationTemplateId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +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.EscalatedNotificationRuleRecipientsConfig; +import org.thingsboard.server.common.data.notification.rule.NotificationRule; +import org.thingsboard.server.common.data.notification.rule.NotificationRuleConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmAssignmentNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmCommentNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction; +import org.thingsboard.server.common.data.notification.rule.trigger.ApiUsageLimitNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.rule.trigger.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent; +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.NewPlatformVersionNotificationRuleTriggerConfig; +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.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; +import org.thingsboard.server.common.data.notification.template.NotificationTemplate; +import org.thingsboard.server.common.data.notification.template.NotificationTemplateConfig; +import org.thingsboard.server.common.data.notification.template.WebDeliveryMethodNotificationTemplate; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.thingsboard.common.util.JacksonUtil.newObjectNode; +import static org.thingsboard.server.dao.DaoUtil.toUUIDs; + +@Service +@RequiredArgsConstructor +public class DefaultNotifications { + + public static final DefaultNotification maintenanceWork = DefaultNotification.builder() + .name("Maintenance work notification") + .subject("Infrastructure maintenance") + .text("Maintenance work is scheduled for tomorrow (7:00 a.m. - 9:00 a.m. UTC)") + .build(); + public static final DefaultNotification entitiesLimitForSysadmin = DefaultNotification.builder() + .name("Entities count limit notification for sysadmin") + .type(NotificationType.ENTITIES_LIMIT) + .subject("${entityType}s limit will be reached soon for tenant ${tenantName}") + .text("${entityType}s usage: ${currentCount}/${limit} (${percents}%)") + .icon("warning").color("#D6D634") + .rule(DefaultRule.builder() + .name("Entities count limit (sysadmin)") + .triggerConfig(EntitiesLimitNotificationRuleTriggerConfig.builder() + .entityTypes(null).threshold(0.8f) + .build()) + .description("Send notification to system admins when count of entities of some type reached 80% threshold of the limit for a tenant") + .build()) + .build(); + public static final DefaultNotification entitiesLimitForTenant = entitiesLimitForSysadmin.toBuilder() + .name("Entities count limit notification for tenant") + .subject("${entityType}s limit will be reached soon") + .rule(entitiesLimitForSysadmin.getRule().toBuilder() + .name("Entities count limit") + .description("Send notification to tenant admins when count of entities of some type reached 80% threshold of the limit") + .build()) + .build(); + public static final DefaultNotification apiFeatureWarningForSysadmin = DefaultNotification.builder() + .name("API feature warning notification for sysadmin") + .type(NotificationType.API_USAGE_LIMIT) + .subject("${feature} feature will be disabled soon for tenant ${tenantName}") + .text("Usage: ${currentValue} out of ${limit} ${unitLabel}s") + .icon("warning").color("#D6D634") + .rule(DefaultRule.builder() + .name("API feature warning (sysadmin)") + .triggerConfig(ApiUsageLimitNotificationRuleTriggerConfig.builder() + .apiFeatures(null) + .notifyOn(Set.of(ApiUsageStateValue.WARNING)) + .build()) + .description("Send notification to system admins on API feature usage WARNING state for a tenant") + .build()) + .build(); + public static final DefaultNotification apiFeatureWarningForTenant = apiFeatureWarningForSysadmin.toBuilder() + .name("API feature warning notification for tenant") + .subject("${feature} feature will be disabled soon") + .rule(apiFeatureWarningForSysadmin.getRule().toBuilder() + .name("API feature warning") + .description("Send notification to tenant admins on API feature usage WARNING state") + .build()) + .build(); + public static final DefaultNotification apiFeatureDisabledForSysadmin = DefaultNotification.builder() + .name("API feature disabled notification for sysadmin") + .type(NotificationType.API_USAGE_LIMIT) + .subject("${feature} feature was disabled for tenant ${tenantName}") + .text("Used ${currentValue} out of ${limit} ${unitLabel}s") + .icon("block").color("#e91a1a") + .rule(DefaultRule.builder() + .name("API feature disabled (sysadmin)") + .triggerConfig(ApiUsageLimitNotificationRuleTriggerConfig.builder() + .apiFeatures(null) + .notifyOn(Set.of(ApiUsageStateValue.DISABLED)) + .build()) + .description("Send notification to system admins when API feature is disabled for a tenant") + .build()) + .build(); + public static final DefaultNotification apiFeatureDisabledForTenant = apiFeatureDisabledForSysadmin.toBuilder() + .name("API feature disabled notification for tenant") + .subject("${feature} feature was disabled") + .rule(apiFeatureDisabledForSysadmin.getRule().toBuilder() + .name("API feature disabled") + .description("Send notification to tenant admins when API feature is disabled") + .build()) + .build(); + public static final DefaultNotification newPlatformVersion = DefaultNotification.builder() + .name("New platform version notification") + .type(NotificationType.NEW_PLATFORM_VERSION) + .subject("New version ${latestVersion} is available") + .text("Current platform version is ${currentVersion}") + .button("Open release notes").link("${latestVersionReleaseNotesUrl}") + .rule(DefaultRule.builder() + .name("New platform version") + .triggerConfig(new NewPlatformVersionNotificationRuleTriggerConfig()) + .description("Send notification to system admins and tenant admins when new platform version is available") + .build()) + .build(); + + public static final DefaultNotification newAlarm = DefaultNotification.builder() + .name("New alarm notification") + .type(NotificationType.ALARM) + .subject("New alarm '${alarmType}'") + .text("Severity: ${alarmSeverity}, originator: ${alarmOriginatorEntityType} '${alarmOriginatorName}'") + .icon("notifications").color(null) + .rule(DefaultRule.builder() + .name("New alarm") + .triggerConfig(AlarmNotificationRuleTriggerConfig.builder() + .alarmTypes(null) + .alarmSeverities(null) + .notifyOn(Set.of(AlarmAction.CREATED)) + .build()) + .description("Send notification to tenant admins when an alarm is created") + .build()) + .build(); + public static final DefaultNotification alarmUpdate = DefaultNotification.builder() + .name("Alarm update notification") + .type(NotificationType.ALARM) + .subject("Alarm '${alarmType}' - ${action}") + .text("Severity: ${alarmSeverity}, originator: ${alarmOriginatorEntityType} '${alarmOriginatorName}'") + .icon("notifications").color(null) + .rule(DefaultRule.builder() + .name("Alarm update") + .triggerConfig(AlarmNotificationRuleTriggerConfig.builder() + .alarmTypes(null) + .alarmSeverities(null) + .notifyOn(Set.of(AlarmAction.SEVERITY_CHANGED, AlarmAction.ACKNOWLEDGED, AlarmAction.CLEARED)) + .build()) + .description("Send notification to tenant admins when any alarm is updated or cleared") + .build()) + .build(); + public static final DefaultNotification deviceAction = DefaultNotification.builder() + .name("Device action notification") + .type(NotificationType.ENTITY_ACTION) + .subject("${entityType} was ${actionType}") + .text("${entityType} '${entityName}' was ${actionType} by user ${userEmail}") + .icon("info").color(null) + .button("Go to device").link("/devices/${entityId}") + .rule(DefaultRule.builder() + .name("Device created") + .triggerConfig(EntityActionNotificationRuleTriggerConfig.builder() + .entityTypes(Set.of(EntityType.DEVICE)) + .created(true) + .updated(false) + .deleted(false) + .build()) + .description("Send notification to tenant admins when device is created") + .build()) + .build(); + public static final DefaultNotification deviceActivity = DefaultNotification.builder() + .name("Device activity notification") + .type(NotificationType.DEVICE_ACTIVITY) + .subject("Device '${deviceName}' became ${eventType}") + .text("Device '${deviceName}' of type '${deviceType}' is now ${eventType}") + .icon("info").color(null) + .button("Go to device").link("/devices/${deviceId}") + .rule(DefaultRule.builder() + .name("Device activity status change") + .triggerConfig(DeviceActivityNotificationRuleTriggerConfig.builder() + .devices(null) + .deviceProfiles(null) + .notifyOn(Set.of(DeviceEvent.ACTIVE, DeviceEvent.INACTIVE)) + .build()) + .description("Send notification to tenant admins when any device changes its activity state") + .build()) + .build(); + public static final DefaultNotification alarmComment = DefaultNotification.builder() + .name("Alarm comment notification") + .type(NotificationType.ALARM_COMMENT) + .subject("Comment on '${alarmType}' alarm") + .text("${userEmail} ${action} comment: ${comment}") + .icon("people").color(null) + .rule(DefaultRule.builder() + .name("Comment on active alarm") + .triggerConfig(AlarmCommentNotificationRuleTriggerConfig.builder() + .alarmTypes(null) + .alarmSeverities(null) + .alarmStatuses(Set.of(AlarmSearchStatus.ACTIVE)) + .onlyUserComments(true) + .notifyOnCommentUpdate(false) + .build()) + .description("Send notification to tenant admins when comment is added by user on active alarm") + .build()) + .build(); + public static final DefaultNotification alarmAssignment = DefaultNotification.builder() + .name("Alarm assigned notification") + .type(NotificationType.ALARM_ASSIGNMENT) + .subject("Alarm '${alarmType}' (${alarmSeverity}) was assigned to user") + .text("${userEmail} assigned alarm on ${alarmOriginatorEntityType} '${alarmOriginatorName}' to ${assigneeEmail}") + .icon("person").color(null) + .rule(DefaultRule.builder() + .name("Alarm assignment") + .triggerConfig(AlarmAssignmentNotificationRuleTriggerConfig.builder() + .alarmTypes(null) + .alarmSeverities(null) + .alarmStatuses(null) + .notifyOn(Set.of(AlarmAssignmentNotificationRuleTriggerConfig.Action.ASSIGNED)) + .build()) + .description("Send notification to user when any alarm was assigned to him") + .build()) + .build(); + public static final DefaultNotification ruleEngineComponentLifecycleFailure = DefaultNotification.builder() + .name("Rule chain/node lifecycle failure notification") + .type(NotificationType.RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT) + .subject("${action:capitalize} failure in Rule chain '${ruleChainName}'") + .text("${componentType} '${componentName}' failed to ${action}") + .icon("warning").color(null) + .button("Go to rule chain").link("/ruleChains/${ruleChainId}") + .rule(DefaultRule.builder() + .name("Rule node initialization failure") + .triggerConfig(RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.builder() + .ruleChains(null) + .ruleChainEvents(Set.of(ComponentLifecycleEvent.STARTED, ComponentLifecycleEvent.UPDATED, ComponentLifecycleEvent.STOPPED)) + .onlyRuleChainLifecycleFailures(true) + .trackRuleNodeEvents(true) + .ruleNodeEvents(Set.of(ComponentLifecycleEvent.STARTED, ComponentLifecycleEvent.UPDATED, ComponentLifecycleEvent.STOPPED)) + .onlyRuleNodeLifecycleFailures(true) + .build()) + .description("Send notification to tenant admins when any Rule chain or Rule node failed to start, update or stop") + .build()) + .build(); + + private final NotificationTemplateService templateService; + private final NotificationRuleService ruleService; + + public final void create(TenantId tenantId, DefaultNotification defaultNotification, NotificationTargetId... targets) { + NotificationTemplate template = defaultNotification.toTemplate(); + template.setTenantId(tenantId); + template = templateService.saveNotificationTemplate(tenantId, template); + + if (defaultNotification.getRule() != null && targets.length > 0) { + NotificationRule rule = defaultNotification.toRule(template.getId(), targets); + rule.setTenantId(tenantId); + rule = ruleService.saveNotificationRule(tenantId, rule); + } + } + + @Data + @Builder(toBuilder = true) + public static class DefaultNotification { + + private final String name; + private final NotificationType type; + private final String subject; + private final String text; + private final String icon; + private final String color; + private final String button; + private final String link; + + private final DefaultRule rule; + + public NotificationTemplate toTemplate() { + NotificationTemplate template = new NotificationTemplate(); + template.setName(name); + template.setNotificationType(type != null ? type : NotificationType.GENERAL); + + NotificationTemplateConfig templateConfig = new NotificationTemplateConfig(); + WebDeliveryMethodNotificationTemplate webTemplate = new WebDeliveryMethodNotificationTemplate(); + webTemplate.setSubject(subject); + webTemplate.setBody(text); + ObjectNode additionalConfig = newObjectNode(); + ObjectNode iconConfig = newObjectNode(); + additionalConfig.set("icon", iconConfig); + ObjectNode buttonConfig = newObjectNode(); + additionalConfig.set("actionButtonConfig", buttonConfig); + if (icon != null) { + iconConfig.put("enabled", true) + .put("icon", icon) + .put("color", color != null ? color : "#757575"); + } else { + iconConfig.put("enabled", false); + } + if (button != null) { + buttonConfig.put("enabled", true) + .put("text", button) + .put("linkType", "LINK") + .put("link", link); + } else { + buttonConfig.put("enabled", false); + } + webTemplate.setAdditionalConfig(additionalConfig); + webTemplate.setEnabled(true); + templateConfig.setDeliveryMethodsTemplates(Map.of( + NotificationDeliveryMethod.WEB, webTemplate + )); + template.setConfiguration(templateConfig); + return template; + } + + public NotificationRule toRule(NotificationTemplateId templateId, NotificationTargetId... targets) { + DefaultRule defaultRule = this.rule; + NotificationRule rule = new NotificationRule(); + rule.setName(name); + rule.setTemplateId(templateId); + rule.setTriggerType(defaultRule.getTriggerConfig().getTriggerType()); + rule.setTriggerConfig(defaultRule.getTriggerConfig()); + if (rule.getTriggerType() == NotificationRuleTriggerType.ALARM) { + EscalatedNotificationRuleRecipientsConfig recipientsConfig = new EscalatedNotificationRuleRecipientsConfig(); + recipientsConfig.setTriggerType(rule.getTriggerType()); + recipientsConfig.setEscalationTable(Map.of(0, toUUIDs(List.of(targets)))); + rule.setRecipientsConfig(recipientsConfig); + } else { + DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); + recipientsConfig.setTriggerType(rule.getTriggerType()); + recipientsConfig.setTargets(toUUIDs(List.of(targets))); + rule.setRecipientsConfig(recipientsConfig); + } + NotificationRuleConfig additionalConfig = new NotificationRuleConfig(); + additionalConfig.setDescription(defaultRule.getDescription()); + rule.setAdditionalConfig(additionalConfig); + return rule; + } + + } + + @Data + @Builder(toBuilder = true) + public static class DefaultRule { + private final String name; + private final NotificationRuleTriggerConfig triggerConfig; + private final String description; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 5a26715154..76947c5043 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/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.page.PageData; 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.RelationTypeGroup; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; @@ -50,7 +51,9 @@ import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleChainUpdateResult; import org.thingsboard.server.common.data.rule.RuleNode; 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.EntityCountService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; @@ -90,6 +93,9 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Autowired private RuleNodeDao ruleNodeDao; + @Autowired + private EntityCountService entityCountService; + @Autowired private DataValidator ruleChainValidator; @@ -98,7 +104,11 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC public RuleChain saveRuleChain(RuleChain ruleChain) { ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); try { - return ruleChainDao.save(ruleChain.getTenantId(), ruleChain); + RuleChain savedRuleChain = ruleChainDao.save(ruleChain.getTenantId(), ruleChain); + if (ruleChain.getId() == null) { + entityCountService.publishCountEntityEvictEvent(ruleChain.getTenantId(), EntityType.RULE_CHAIN); + } + return savedRuleChain; } catch (Exception e) { checkConstraintViolation(e, "rule_chain_external_id_unq_key", "Rule Chain with such external id already exists!"); throw e; @@ -146,6 +156,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC Map ruleNodeIndexMap = new HashMap<>(); if (nodes != null) { for (RuleNode node : nodes) { + setSingletonMode(node); if (node.getId() != null) { ruleNodeIndexMap.put(node.getId(), nodes.indexOf(node)); } else { @@ -696,6 +707,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC private void checkRuleNodesAndDelete(TenantId tenantId, RuleChainId ruleChainId) { try { + entityCountService.publishCountEntityEvictEvent(tenantId, EntityType.RULE_CHAIN); ruleChainDao.removeById(tenantId, ruleChainId.getId()); } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); @@ -737,6 +749,11 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC return Optional.ofNullable(hasId); } + @Override + public long countByTenantId(TenantId tenantId) { + return ruleChainDao.countByTenantId(tenantId); + } + @Override public EntityType getEntityType() { return EntityType.RULE_CHAIN; @@ -769,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); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java index ac145034a5..5450e29706 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java +++ b/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.RuleChainType; 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.dao.exception.DataValidationException; import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.DataValidator; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantService; import java.util.HashMap; @@ -62,7 +60,7 @@ public class RuleChainDataValidator extends DataValidator { @Override protected void validateCreate(TenantId tenantId, RuleChain data) { - validateNumberOfEntitiesPerTenant(tenantId, EntityType.RULE_CHAIN); + validateNumberOfEntitiesPerTenant(tenantId, EntityType.RULE_CHAIN); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java index f64ac48c53..943697149c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java +++ b/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("scope", entity.getScope().name()) .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) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/SqlComponentDescriptorInsertRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/SqlComponentDescriptorInsertRepository.java index 98eb76da87..08606f6737 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/SqlComponentDescriptorInsertRepository.java +++ b/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) { - 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) { - 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"; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java index 85e3917ceb..04e978c7d4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -312,9 +312,8 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) { QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, null, EntityType.ALARM)); - ctx.append("select count(id) from alarm_info a "); - if (query.isSearchPropagatedAlarms()) { + ctx.append("select count(distinct(a.id)) from alarm_info a "); ctx.append(JOIN_ENTITY_ALARMS); ctx.append("where a.tenant_id = :tenantId and ea.tenant_id = :tenantId"); ctx.addUuidParameter("tenantId", tenantId.getId()); @@ -323,6 +322,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { ctx.addUuidParameter("customerId", customerId.getId()); } } else { + ctx.append("select count(id) from alarm_info a "); ctx.append("where a.tenant_id = :tenantId"); ctx.addUuidParameter("tenantId", tenantId.getId()); if (customerId != null && !customerId.isNullUid()) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/DefaultApiLimitService.java b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/DefaultApiLimitService.java index d58c91c1c7..d5d19aa453 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/usagerecord/DefaultApiLimitService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/usagerecord/DefaultApiLimitService.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.usagerecord; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.CustomerId; @@ -25,8 +24,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityTypeFilter; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; -import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; @@ -36,8 +33,6 @@ public class DefaultApiLimitService implements ApiLimitService { private final EntityService entityService; private final TbTenantProfileCache tenantProfileCache; - @Autowired(required = false) - private NotificationRuleProcessor notificationRuleProcessor; @Override public boolean checkEntitiesLimit(TenantId tenantId, EntityType entityType) { @@ -47,14 +42,6 @@ public class DefaultApiLimitService implements ApiLimitService { EntityTypeFilter filter = new EntityTypeFilter(); filter.setEntityType(entityType); long currentCount = entityService.countEntitiesByQuery(tenantId, new CustomerId(EntityId.NULL_UUID), new EntityCountQuery(filter)); - if (notificationRuleProcessor != null) { - notificationRuleProcessor.process(EntitiesLimitTrigger.builder() - .tenantId(tenantId) - .entityType(entityType) - .currentCount(currentCount) - .limit(limit) - .build()); - } return currentCount < limit; } else { return true; diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index 96fb740088..4aad5fa1b2 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -20,10 +20,13 @@ CREATE INDEX IF NOT EXISTS idx_alarm_originator_created_time ON alarm(originator CREATE INDEX IF NOT EXISTS idx_alarm_tenant_created_time ON alarm(tenant_id, created_time DESC); --- Drop index by 'status' column and replace with new one that has only active alarms; +-- Drop index by 'status' column and replace with new indexes that has only active alarms; CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type_active ON alarm USING btree (originator_id, type) WHERE cleared = false; +CREATE INDEX IF NOT EXISTS idx_alarm_tenant_alarm_type_active + ON alarm USING btree (tenant_id, type) WHERE cleared = false; + CREATE INDEX IF NOT EXISTS idx_alarm_tenant_alarm_type_created_time ON alarm(tenant_id, type, created_time DESC); CREATE INDEX IF NOT EXISTS idx_alarm_tenant_assignee_created_time ON alarm(tenant_id, assignee_id, created_time DESC); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index ddd60ae812..c22feb0695 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -125,7 +125,8 @@ CREATE TABLE IF NOT EXISTS component_descriptor ( name varchar(255), scope varchar(255), search_text varchar(255), - type varchar(255) + type varchar(255), + clustering_mode varchar(255) ); CREATE TABLE IF NOT EXISTS customer ( @@ -187,6 +188,7 @@ CREATE TABLE IF NOT EXISTS rule_node ( type varchar(255), name varchar(255), debug_mode boolean, + singleton_mode boolean, search_text varchar(255), external_id uuid ); diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 429df521b6..e044635e98 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -41,6 +41,9 @@ ${project.build.directory}/windows ThingsBoard Monitoring Service org.thingsboard.monitoring.ThingsboardMonitoringApplication + + 2.6.1 + 2.0.0-M4 @@ -65,10 +68,12 @@ org.eclipse.californium californium-core + ${californium.version} org.eclipse.californium scandium + ${californium.version} org.eclipse.paho @@ -78,6 +83,16 @@ org.apache.httpcomponents httpclient + + org.eclipse.leshan + leshan-client-cf + ${leshan.version} + + + org.eclipse.leshan + leshan-core + ${leshan.version} + org.java-websocket Java-WebSocket diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java b/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java new file mode 100644 index 0000000000..473a17aa00 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java @@ -0,0 +1,171 @@ +/** + * 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.monitoring.client; + +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.californium.core.network.CoapEndpoint; +import org.eclipse.californium.core.network.config.NetworkConfig; +import org.eclipse.californium.core.observe.ObservationStore; +import org.eclipse.californium.scandium.DTLSConnector; +import org.eclipse.californium.scandium.config.DtlsConnectorConfig; +import org.eclipse.leshan.client.californium.LeshanClient; +import org.eclipse.leshan.client.californium.LeshanClientBuilder; +import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory; +import org.eclipse.leshan.client.object.Security; +import org.eclipse.leshan.client.object.Server; +import org.eclipse.leshan.client.resource.BaseInstanceEnabler; +import org.eclipse.leshan.client.resource.DummyInstanceEnabler; +import org.eclipse.leshan.client.resource.ObjectsInitializer; +import org.eclipse.leshan.client.servers.ServerIdentity; +import org.eclipse.leshan.core.californium.EndpointFactory; +import org.eclipse.leshan.core.model.InvalidDDFFileException; +import org.eclipse.leshan.core.model.LwM2mModel; +import org.eclipse.leshan.core.model.ObjectLoader; +import org.eclipse.leshan.core.model.ObjectModel; +import org.eclipse.leshan.core.model.StaticModel; +import org.eclipse.leshan.core.node.codec.DefaultLwM2mDecoder; +import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder; +import org.eclipse.leshan.core.response.ReadResponse; +import org.thingsboard.monitoring.util.ResourceUtils; + +import javax.security.auth.Destroyable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.eclipse.leshan.client.object.Security.noSec; +import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; +import static org.eclipse.leshan.core.LwM2mId.DEVICE; +import static org.eclipse.leshan.core.LwM2mId.SECURITY; +import static org.eclipse.leshan.core.LwM2mId.SERVER; + +@Slf4j +public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable { + + @Getter + @Setter + private LeshanClient leshanClient; + + private static final List supportedResources = Collections.singletonList(0); + + private String data = ""; + + private String serverUri; + private String endpoint; + + public Lwm2mClient(String serverUri, String endpoint) { + this.serverUri = serverUri; + this.endpoint = endpoint; + } + + public Lwm2mClient() { + } + + public void initClient() throws InvalidDDFFileException, IOException { + String[] resources = new String[]{"0.xml", "1.xml", "2.xml", "test-model.xml"}; + List models = new ArrayList<>(); + for (String resourceName : resources) { + models.addAll(ObjectLoader.loadDdfFile(ResourceUtils.getResourceAsStream("lwm2m/models/" + resourceName), resourceName)); + } + + Security security = noSec(serverUri, 123); + NetworkConfig coapConfig = new NetworkConfig().setString(NetworkConfig.Keys.COAP_PORT, StringUtils.substringAfterLast(serverUri, ":")); + + LeshanClient leshanClient; + + LwM2mModel model = new StaticModel(models); + ObjectsInitializer initializer = new ObjectsInitializer(model); + initializer.setInstancesForObject(SECURITY, security); + initializer.setInstancesForObject(SERVER, new Server(123, 300)); + initializer.setInstancesForObject(DEVICE, this); + initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); + DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(); + dtlsConfig.setRecommendedCipherSuitesOnly(true); + dtlsConfig.setClientOnly(); + + DefaultRegistrationEngineFactory engineFactory = new DefaultRegistrationEngineFactory(); + engineFactory.setReconnectOnUpdate(false); + engineFactory.setResumeOnConnect(true); + + EndpointFactory endpointFactory = new EndpointFactory() { + + @Override + public CoapEndpoint createUnsecuredEndpoint(InetSocketAddress address, NetworkConfig coapConfig, + ObservationStore store) { + CoapEndpoint.Builder builder = new CoapEndpoint.Builder(); + builder.setInetSocketAddress(address); + builder.setNetworkConfig(coapConfig); + return builder.build(); + } + + @Override + public CoapEndpoint createSecuredEndpoint(DtlsConnectorConfig dtlsConfig, NetworkConfig coapConfig, + ObservationStore store) { + CoapEndpoint.Builder builder = new CoapEndpoint.Builder(); + DtlsConnectorConfig.Builder dtlsConfigBuilder = new DtlsConnectorConfig.Builder(dtlsConfig); + builder.setConnector(new DTLSConnector(dtlsConfigBuilder.build())); + builder.setNetworkConfig(coapConfig); + return builder.build(); + } + }; + + LeshanClientBuilder builder = new LeshanClientBuilder(endpoint); + builder.setObjects(initializer.createAll()); + builder.setCoapConfig(coapConfig); + builder.setDtlsConfig(dtlsConfig); + builder.setRegistrationEngineFactory(engineFactory); + builder.setEndpointFactory(endpointFactory); + builder.setDecoder(new DefaultLwM2mDecoder(false)); + builder.setEncoder(new DefaultLwM2mEncoder(false)); + leshanClient = builder.build(); + + setLeshanClient(leshanClient); + + leshanClient.start(); + } + + @Override + public List getAvailableResourceIds(ObjectModel model) { + return supportedResources; + } + + @Override + public ReadResponse read(ServerIdentity identity, int resourceId) { + if (supportedResources.contains(resourceId)) { + return ReadResponse.success(resourceId, data); + } + return super.read(identity, resourceId); + } + + @SneakyThrows + public void send(String data, int resource) { + this.data = data; + fireResourcesChange(resource); + } + + @Override + public void destroy() { + if (leshanClient != null) { + leshanClient.destroy(true); + } + } +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java index ed0a4ea331..a3aaa7ec98 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/TransportType.java @@ -20,6 +20,7 @@ import lombok.Getter; import org.thingsboard.monitoring.transport.TransportHealthChecker; import org.thingsboard.monitoring.transport.impl.CoapTransportHealthChecker; import org.thingsboard.monitoring.transport.impl.HttpTransportHealthChecker; +import org.thingsboard.monitoring.transport.impl.Lwm2mTransportHealthChecker; import org.thingsboard.monitoring.transport.impl.MqttTransportHealthChecker; @AllArgsConstructor @@ -28,7 +29,8 @@ public enum TransportType { MQTT(MqttTransportHealthChecker.class), COAP(CoapTransportHealthChecker.class), - HTTP(HttpTransportHealthChecker.class); + HTTP(HttpTransportHealthChecker.class), + LWM2M(Lwm2mTransportHealthChecker.class); private final Class> serviceClass; diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/service/Lwm2mTransportMonitoringConfig.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/service/Lwm2mTransportMonitoringConfig.java new file mode 100644 index 0000000000..4d97de8366 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/service/Lwm2mTransportMonitoringConfig.java @@ -0,0 +1,33 @@ +/** + * 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.monitoring.config.service; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.thingsboard.monitoring.config.TransportType; + +@Component +@ConditionalOnProperty(name = "monitoring.transports.lwm2m.enabled", havingValue = "true") +@ConfigurationProperties(prefix = "monitoring.transports.lwm2m") +public class Lwm2mTransportMonitoringConfig extends TransportMonitoringConfig { + + @Override + public TransportType getTransportType() { + return TransportType.LWM2M; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java index 9717b98f51..0c742e7709 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/transport/TransportHealthChecker.java @@ -67,7 +67,7 @@ public abstract class TransportHealthChecker { log.info("Creating new device '{}'", deviceName); - Device monitoringDevice = new Device(); - monitoringDevice.setName(deviceName); - monitoringDevice.setType("default"); - DeviceData deviceData = new DeviceData(); - deviceData.setConfiguration(new DefaultDeviceConfiguration()); - deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); - return tbClient.saveDevice(monitoringDevice); + return createDevice(config.getTransportType(), deviceName, tbClient); }); deviceId = device.getId(); target.getDevice().setId(deviceId.toString()); @@ -134,10 +141,58 @@ public final class TransportMonitoringService { deviceId = new DeviceId(deviceConfig.getId()); } - log.info("Loading credentials for device {}", deviceId); + log.info("Using device {} for {} monitoring", deviceId, config.getTransportType()); DeviceCredentials credentials = tbClient.getDeviceCredentialsByDeviceId(deviceId) .orElseThrow(() -> new IllegalArgumentException("No credentials found for device " + deviceId)); target.getDevice().setCredentials(credentials); } + private Device createDevice(TransportType transportType, String name, TbClient tbClient) { + Device device = new Device(); + device.setName(name); + + DeviceCredentials credentials = new DeviceCredentials(); + credentials.setCredentialsId(RandomStringUtils.randomAlphabetic(20)); + + DeviceData deviceData = new DeviceData(); + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + if (transportType != TransportType.LWM2M) { + device.setType("default"); + deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); + credentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + } else { + tbClient.getResources(new PageLink(1, 0, "lwm2m monitoring")).getData() + .stream().findFirst() + .orElseGet(() -> { + TbResource newResource = ResourceUtils.getResource("lwm2m/resource.json", TbResource.class); + log.info("Creating LwM2M resource"); + return tbClient.saveResource(newResource); + }); + String profileName = "LwM2M Monitoring"; + DeviceProfile profile = tbClient.getDeviceProfiles(new PageLink(1, 0, profileName)).getData() + .stream().findFirst() + .orElseGet(() -> { + DeviceProfile newProfile = ResourceUtils.getResource("lwm2m/device_profile.json", DeviceProfile.class); + newProfile.setName(profileName); + log.info("Creating LwM2M device profile"); + return tbClient.saveDeviceProfile(newProfile); + }); + device.setType(profileName); + device.setDeviceProfileId(profile.getId()); + deviceData.setTransportConfiguration(new Lwm2mDeviceTransportConfiguration()); + + credentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS); + LwM2MDeviceCredentials lwm2mCreds = new LwM2MDeviceCredentials(); + NoSecClientCredential client = new NoSecClientCredential(); + client.setEndpoint(credentials.getCredentialsId()); + lwm2mCreds.setClient(client); + LwM2MBootstrapClientCredentials bootstrap = new LwM2MBootstrapClientCredentials(); + bootstrap.setBootstrapServer(new NoSecBootstrapClientCredential()); + bootstrap.setLwm2mServer(new NoSecBootstrapClientCredential()); + lwm2mCreds.setBootstrap(bootstrap); + credentials.setCredentialsValue(JacksonUtil.toString(lwm2mCreds)); + } + return tbClient.saveDeviceWithCredentials(device, credentials).get(); + } + } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/Lwm2mTransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/Lwm2mTransportHealthChecker.java new file mode 100644 index 0000000000..6487003e0c --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/transport/impl/Lwm2mTransportHealthChecker.java @@ -0,0 +1,72 @@ +/** + * 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.monitoring.transport.impl; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Service; +import org.thingsboard.monitoring.client.Lwm2mClient; +import org.thingsboard.monitoring.config.MonitoringTargetConfig; +import org.thingsboard.monitoring.config.TransportType; +import org.thingsboard.monitoring.config.service.Lwm2mTransportMonitoringConfig; +import org.thingsboard.monitoring.transport.TransportHealthChecker; + +@Service +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +@Slf4j +public class Lwm2mTransportHealthChecker extends TransportHealthChecker { + + private Lwm2mClient lwm2mClient; + + protected Lwm2mTransportHealthChecker(Lwm2mTransportMonitoringConfig config, MonitoringTargetConfig target) { + super(config, target); + } + + @Override + protected void initClient() throws Exception { + if (lwm2mClient == null || lwm2mClient.getLeshanClient() == null || lwm2mClient.isDestroyed()) { + String endpoint = target.getDevice().getCredentials().getCredentialsId(); + lwm2mClient = new Lwm2mClient(target.getBaseUrl(), endpoint); + lwm2mClient.initClient(); + log.debug("Initialized LwM2M client for endpoint '{}'", endpoint); + } + } + + @Override + protected void sendTestPayload(String payload) throws Exception { + lwm2mClient.send(payload, 0); + } + + @Override + protected String createTestPayload(String testValue) { + return testValue; + } + + @Override + protected void destroyClient() throws Exception { + if (lwm2mClient != null) { + lwm2mClient.destroy(); + lwm2mClient = null; + } + } + + @Override + protected TransportType getTransportType() { + return TransportType.LWM2M; + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/util/ResourceUtils.java b/monitoring/src/main/java/org/thingsboard/monitoring/util/ResourceUtils.java new file mode 100644 index 0000000000..4cc1efe8cb --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/util/ResourceUtils.java @@ -0,0 +1,42 @@ +/** + * 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.monitoring.util; + +import lombok.SneakyThrows; +import org.thingsboard.common.util.JacksonUtil; + +import java.io.InputStream; + +public class ResourceUtils { + + @SneakyThrows + public static T getResource(String path, Class type) { + InputStream resource = ResourceUtils.class.getClassLoader().getResourceAsStream(path); + if (resource == null) { + throw new IllegalArgumentException("Resource not found for path " + path); + } + return JacksonUtil.OBJECT_MAPPER.readValue(resource, type); + } + + public static InputStream getResourceAsStream(String path) { + InputStream resource = ResourceUtils.class.getClassLoader().getResourceAsStream(path); + if (resource == null) { + throw new IllegalArgumentException("Resource not found for path " + path); + } + return resource; + } + +} diff --git a/monitoring/src/main/resources/lwm2m/device_profile.json b/monitoring/src/main/resources/lwm2m/device_profile.json new file mode 100644 index 0000000000..7f93a7e6b6 --- /dev/null +++ b/monitoring/src/main/resources/lwm2m/device_profile.json @@ -0,0 +1,59 @@ +{ + "name": "LwM2M Monitoring", + "type": "DEFAULT", + "image": null, + "defaultQueueName": null, + "transportType": "LWM2M", + "provisionType": "DISABLED", + "description": "", + "profileData": { + "configuration": { + "type": "DEFAULT" + }, + "transportConfiguration": { + "observeAttr": { + "observe": [ + "/3_1.0/0/0" + ], + "attribute": [], + "telemetry": [ + "/3_1.0/0/0" + ], + "keyName": { + "/3_1.0/0/0": "testData" + }, + "attributeLwm2m": {} + }, + "bootstrap": [ + { + "shortServerId": 123, + "bootstrapServerIs": false, + "host": "0.0.0.0", + "port": 5685, + "clientHoldOffTime": 1, + "serverPublicKey": "", + "serverCertificate": "", + "bootstrapServerAccountTimeout": 0, + "lifetime": 300, + "defaultMinPeriod": 1, + "notifIfDisabled": true, + "binding": "U", + "securityMode": "NO_SEC" + } + ], + "clientLwM2mSettings": { + "clientOnlyObserveAfterConnect": 1, + "fwUpdateStrategy": 1, + "swUpdateStrategy": 1, + "powerMode": "DRX", + "compositeOperationsSupport": false + }, + "bootstrapServerUpdateEnable": false, + "type": "LWM2M" + }, + "alarms": null, + "provisionConfiguration": { + "type": "DISABLED" + } + } +} \ No newline at end of file diff --git a/monitoring/src/main/resources/lwm2m/models/0.xml b/monitoring/src/main/resources/lwm2m/models/0.xml new file mode 100644 index 0000000000..d122984c16 --- /dev/null +++ b/monitoring/src/main/resources/lwm2m/models/0.xml @@ -0,0 +1,364 @@ + + + + + LWM2M Security + + 0 + urn:oma:lwm2m:oma:0:1.2 + 1.1 + 1.2 + Multiple + Mandatory + + + LWM2M Server URI + + Single + Mandatory + String + 0..255 + + + + + Bootstrap-Server + + Single + Mandatory + Boolean + + + + + + Security Mode + + Single + Mandatory + Integer + 0..4 + + + + + Public Key or Identity + + Single + Mandatory + Opaque + + + + + + Server Public Key + + Single + Mandatory + Opaque + + + + + + Secret Key + + Single + Mandatory + Opaque + + + + + + SMS Security Mode + + Single + Optional + Integer + 0..255 + + + + + SMS Binding Key Parameters + + Single + Optional + Opaque + 6 + + + + + SMS Binding Secret Key(s) + + Single + Optional + Opaque + 16,32,48 + + + + + LwM2M Server SMS Number + + Single + Optional + String + + + + + + Short Server ID + + Single + Optional + Integer + 1..65534 + + + + + Client Hold Off Time + + Single + Optional + Integer + + s + + + + Bootstrap-Server Account Timeout + + Single + Optional + Integer + + s + + + + Matching Type + + Single + Optional + Integer + 0..3 + + + + + SNI + + Single + Optional + String + + + + + + Certificate Usage + + Single + Optional + Integer + 0..3 + + + + + DTLS/TLS Ciphersuite + + Multiple + Optional + Integer + + + + + OSCORE Security Mode + + Single + Optional + Objlnk + + + + + + Groups To Use by Client + + Multiple + Optional + Integer + 0..65535 + + + + + Signature Algorithms Supported by Server + + Multiple + Optional + Integer + 0..65535 + + + + Signature Algorithms To Use by Client + + Multiple + Optional + Integer + 0..65535 + + + + + Signature Algorithm Certs Supported by Server + + Multiple + Optional + Integer + 0..65535 + + + + + TLS 1.3 Features To Use by Client + + Single + Optional + Integer + 0..65535 + + + + + TLS Extensions Supported by Server + + Single + Optional + Integer + 0..65535 + + + + + TLS Extensions To Use by Client + + Single + Optional + Integer + 0..65535 + + + + + Secondary LwM2M Server URI + + Multiple + Optional + String + 0..255 + + + + MQTT Server + + Single + Optional + Objlnk + + + + + LwM2M COSE Security + + Multiple + Optional + Objlnk + + + + + RDS Destination Port + + Single + Optional + Integer + 0..15 + + + + RDS Source Port + + Single + Optional + Integer + 0..15 + + + + RDS Application ID + + Single + Optional + String + + + + + + + + diff --git a/monitoring/src/main/resources/lwm2m/models/1.xml b/monitoring/src/main/resources/lwm2m/models/1.xml new file mode 100644 index 0000000000..a81caa27b0 --- /dev/null +++ b/monitoring/src/main/resources/lwm2m/models/1.xml @@ -0,0 +1,319 @@ + + + + + LwM2M Server + + 1 + urn:oma:lwm2m:oma:1:1.2 + 1.2 + 1.2 + Multiple + Mandatory + + + Short Server ID + R + Single + Mandatory + Integer + 1..65534 + + + + + Lifetime + RW + Single + Mandatory + Integer + + s + + + + Default Minimum Period + RW + Single + Optional + Integer + + s + + + + Default Maximum Period + RW + Single + Optional + Integer + + s + + + + Disable + E + Single + Optional + + + + + + + Disable Timeout + RW + Single + Optional + Integer + + s + + + + Notification Storing When Disabled or Offline + RW + Single + Mandatory + Boolean + + + + + + Binding + RW + Single + Mandatory + String + + + + + + Registration Update Trigger + E + Single + Mandatory + + + + + + + Bootstrap-Request Trigger + E + Single + Optional + + + + + + + APN Link + RW + Single + Optional + Objlnk + + + + + + TLS-DTLS Alert Code + R + Single + Optional + Integer + 0..255 + + + + + Last Bootstrapped + R + Single + Optional + Time + + + + + + Registration Priority Order + R + Single + Optional + Integer + + + + + + Initial Registration Delay Timer + RW + Single + Optional + Integer + + s + + + + Registration Failure Block + R + Single + Optional + Boolean + + + + + + Bootstrap on Registration Failure + R + Single + Optional + Boolean + + + + + + Communication Retry Count + RW + Single + Optional + Integer + + + + + + Communication Retry Timer + RW + Single + Optional + Integer + + s + + + + Communication Sequence Delay Timer + RW + Single + Optional + Integer + + s + + + + Communication Sequence Retry Count + RW + Single + Optional + Integer + + + + + + Trigger + RW + Single + Optional + Boolean + + + + + + Preferred Transport + RW + Single + Optional + String + The possible values are those listed in the LwM2M Core Specification + + + + Mute Send + RW + Single + Optional + Boolean + + + + + + Alternate APN Links + RW + Multiple + Optional + Objlnk + + + + + + Supported Server Versions + RW + Multiple + Optional + String + + + + + + Default Notification Mode + RW + Single + Optional + Integer + 0..1 + + + + + Profile ID Hash Algorithm + RW + Single + Optional + Integer + 0..255 + + + + + + + diff --git a/monitoring/src/main/resources/lwm2m/models/2.xml b/monitoring/src/main/resources/lwm2m/models/2.xml new file mode 100644 index 0000000000..79b2ed1321 --- /dev/null +++ b/monitoring/src/main/resources/lwm2m/models/2.xml @@ -0,0 +1,83 @@ + + + + + LwM2M Access Control + + 2 + urn:oma:lwm2m:oma:2:1.1 + 1.0 + 1.1 + Multiple + Optional + + + Object ID + R + Single + Mandatory + Integer + 1..65534 + + + + + Object Instance ID + R + Single + Mandatory + Integer + 0..65535 + + + + + ACL + RW + Multiple + Optional + Integer + 0..31 + + + + + Access Control Owner + RW + Single + Mandatory + Integer + 0..65535 + + + + + + + diff --git a/monitoring/src/main/resources/lwm2m/models/test-model.xml b/monitoring/src/main/resources/lwm2m/models/test-model.xml new file mode 100644 index 0000000000..c8e49ce32c --- /dev/null +++ b/monitoring/src/main/resources/lwm2m/models/test-model.xml @@ -0,0 +1,45 @@ + + + + + LwM2M Monitoring + + + 3 + urn:oma:lwm2m:oma:3:1.0 + 1.1 + 1.0 + Single + Mandatory + + + Test data + R + Single + Optional + String + + + + + + + + diff --git a/monitoring/src/main/resources/lwm2m/resource.json b/monitoring/src/main/resources/lwm2m/resource.json new file mode 100644 index 0000000000..b624fa400e --- /dev/null +++ b/monitoring/src/main/resources/lwm2m/resource.json @@ -0,0 +1,6 @@ +{ + "title": "", + "resourceType": "LWM2M_MODEL", + "fileName": "test-model.xml", + "data": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLQoKICAgIENvcHlyaWdodCDCqSAyMDE2LTIwMjIgVGhlIFRoaW5nc2JvYXJkIEF1dGhvcnMKCiAgICBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKICAgIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KICAgIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiAgICBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiAgICBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogICAgV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiAgICBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiAgICBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCi0tPgo8TFdNMk0geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIKICAgICAgIHhzaTpub05hbWVzcGFjZVNjaGVtYUxvY2F0aW9uPSJodHRwOi8vd3d3Lm9wZW5tb2JpbGVhbGxpYW5jZS5vcmcvdGVjaC9wcm9maWxlcy9MV00yTS12MV8xLnhzZCI+CiAgICA8T2JqZWN0IE9iamVjdFR5cGU9Ik1PRGVmaW5pdGlvbiI+CiAgICAgICAgPE5hbWU+THdNMk0gTW9uaXRvcmluZzwvTmFtZT4KICAgICAgICA8RGVzY3JpcHRpb24xPgogICAgICAgICAgICA8IVtDREFUQVtdXT48L0Rlc2NyaXB0aW9uMT4KICAgICAgICA8T2JqZWN0SUQ+MzwvT2JqZWN0SUQ+CiAgICAgICAgPE9iamVjdFVSTj51cm46b21hOmx3bTJtOm9tYTozOjEuMDwvT2JqZWN0VVJOPgogICAgICAgIDxMV00yTVZlcnNpb24+MS4xPC9MV00yTVZlcnNpb24+CiAgICAgICAgPE9iamVjdFZlcnNpb24+MS4wPC9PYmplY3RWZXJzaW9uPgogICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgIDxNYW5kYXRvcnk+TWFuZGF0b3J5PC9NYW5kYXRvcnk+CiAgICAgICAgPFJlc291cmNlcz4KICAgICAgICAgICAgPEl0ZW0gSUQ9IjAiPgogICAgICAgICAgICAgICAgPE5hbWU+VGVzdCBkYXRhPC9OYW1lPgogICAgICAgICAgICAgICAgPE9wZXJhdGlvbnM+UjwvT3BlcmF0aW9ucz4KICAgICAgICAgICAgICAgIDxNdWx0aXBsZUluc3RhbmNlcz5TaW5nbGU8L011bHRpcGxlSW5zdGFuY2VzPgogICAgICAgICAgICAgICAgPE1hbmRhdG9yeT5PcHRpb25hbDwvTWFuZGF0b3J5PgogICAgICAgICAgICAgICAgPFR5cGU+U3RyaW5nPC9UeXBlPgogICAgICAgICAgICAgICAgPFJhbmdlRW51bWVyYXRpb24+PC9SYW5nZUVudW1lcmF0aW9uPgogICAgICAgICAgICAgICAgPFVuaXRzPjwvVW5pdHM+CiAgICAgICAgICAgICAgICA8RGVzY3JpcHRpb24+PCFbQ0RBVEFbVGVzdCBkYXRhXV0+PC9EZXNjcmlwdGlvbj4KICAgICAgICAgICAgPC9JdGVtPgogICAgICAgIDwvUmVzb3VyY2VzPgogICAgICAgIDxEZXNjcmlwdGlvbjI+PC9EZXNjcmlwdGlvbjI+CiAgICA8L09iamVjdD4KPC9MV00yTT4K" +} \ No newline at end of file diff --git a/monitoring/src/main/resources/tb-monitoring.yml b/monitoring/src/main/resources/tb-monitoring.yml index 403b29acc2..d89886cd72 100644 --- a/monitoring/src/main/resources/tb-monitoring.yml +++ b/monitoring/src/main/resources/tb-monitoring.yml @@ -91,6 +91,21 @@ monitoring: # monitoring.transports.http.targets[1].base_url, monitoring.transports.http.targets[1].device.id, # monitoring.transports.http.targets[2].base_url, monitoring.transports.http.targets[2].device.id, etc. + lwm2m: + # Enable LwM2M checks + enabled: '${LWM2M_TRANSPORT_MONITORING_ENABLED:true}' + # LwM2M request timeout in milliseconds + request_timeout_ms: '${LWM2M_REQUEST_TIMEOUT_MS:4000}' + targets: + # LwM2M base url, coap://DOMAIN:5685 by default + - base_url: '${LWM2M_TRANSPORT_BASE_URL:coap://${monitoring.domain}:5685}' + # LwM2M device to push telemetry for. If not set - device will be found or created automatically + device: + id: '${LWM2M_TRANSPORT_TARGET_DEVICE_ID:}' + # To add more targets, use following environment variables: + # monitoring.transports.lwm2m.targets[1].base_url, monitoring.transports.lwm2m.targets[1].device.id, + # monitoring.transports.lwm2m.targets[2].base_url, monitoring.transports.lwm2m.targets[2].device.id, etc. + notification_channels: slack: # Enable notifying via Slack diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractBasePage.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractBasePage.java index 6384e17426..35639bc19f 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractBasePage.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractBasePage.java @@ -117,7 +117,7 @@ abstract public class AbstractBasePage { try { return wait.until(ExpectedConditions.not(ExpectedConditions.visibilityOfElementLocated(By.xpath(locator)))); } catch (WebDriverException e) { - return fail("Element is present"); + return fail("Element is present: " + locator); } } @@ -125,7 +125,7 @@ abstract public class AbstractBasePage { try { return wait.until(ExpectedConditions.not(ExpectedConditions.visibilityOfAllElementsLocatedBy(By.xpath(locator)))); } catch (WebDriverException e) { - return fail("Elements is present"); + return fail("Elements is present: " + locator); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java index 195d3367b8..82b251c288 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java @@ -48,6 +48,7 @@ import java.io.ByteArrayInputStream; import java.net.MalformedURLException; import java.net.URL; import java.time.Duration; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -138,13 +139,15 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest { } public RuleChain getRuleChainByName(String name) { - try { - return testRestClient.getRuleChains(pageLink).getData().stream() - .filter(s -> s.getName().equals(name)).collect(Collectors.toList()).get(0); - } catch (Exception e) { - log.error("No such rule chain with name: " + name); - return null; - } + return testRestClient.getRuleChains(pageLink).getData().stream() + .filter(s -> s.getName().equals(name)) + .findFirst().orElse(null); + } + + public List getRuleChainsByName(String name) { + return testRestClient.getRuleChains(pageLink).getData().stream() + .filter(s -> s.getName().equals(name)) + .collect(Collectors.toList()); } public Customer getCustomerByName(String name) { @@ -184,20 +187,38 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest { } } - public void clearStorage() { - getWebStorage().getLocalStorage().clear(); - getWebStorage().getSessionStorage().clear(); + public JavascriptExecutor getJs() { + return js = (JavascriptExecutor) driver; } public void assertIsDisplayed(WebElement element) { assertThat(element.isDisplayed()).as(element + " is displayed").isTrue(); } - public JavascriptExecutor getJs() { - return js = (JavascriptExecutor) driver; + public void assertIsDisable(WebElement element) { + assertThat(element.isEnabled()).as(element + " is disabled").isFalse(); + } + + public void deleteRuleChainByName(String ruleChainName) { + List ruleChains = getRuleChainsByName(ruleChainName); + if (!ruleChains.isEmpty()) { + ruleChains.forEach(rc -> testRestClient.deleteRuleChain(rc.getId())); + } + } + + public void setRootRuleChain(String ruleChainName) { + List ruleChains = getRuleChainsByName(ruleChainName); + if (!ruleChains.isEmpty()) { + testRestClient.setRootRuleChain(ruleChains.stream().findFirst().get().getId()); + } } public WebStorage getWebStorage() { return webStorage = (WebStorage) driver; } + + public void clearStorage() { + getWebStorage().getLocalStorage().clear(); + getWebStorage().getSessionStorage().clear(); + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/OtherPageElements.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/OtherPageElements.java index 7cb5da3bc6..3fd43aa140 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/OtherPageElements.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/OtherPageElements.java @@ -33,7 +33,7 @@ public class OtherPageElements extends AbstractBasePage { private static final String ENTITY_COUNT = "//div[@class='mat-paginator-range-label']"; private static final String WARNING_DELETE_POPUP_YES = "//tb-confirm-dialog//button[2]"; private static final String WARNING_DELETE_POPUP_TITLE = "//tb-confirm-dialog/h2"; - private static final String REFRESH_BTN = "//mat-icon[contains(text(),'refresh')]/.."; + private static final String REFRESH_BTN = "//mat-icon[contains(text(),'refresh')]/parent::button"; private static final String HELP_BTN = "//mat-icon[contains(text(),'help')]/ancestor::button"; private static final String CHECKBOX = "//mat-row//span[contains(text(),'%s')]/../..//mat-checkbox"; private static final String CHECKBOXES = "//tbody//mat-checkbox"; diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/OtherPageElementsHelper.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/OtherPageElementsHelper.java index 815e118a9b..7323c2cd75 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/OtherPageElementsHelper.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/OtherPageElementsHelper.java @@ -15,8 +15,6 @@ */ package org.thingsboard.server.msa.ui.pages; -import org.openqa.selenium.By; -import org.openqa.selenium.Keys; import org.openqa.selenium.WebDriver; public class OtherPageElementsHelper extends OtherPageElements { @@ -34,7 +32,7 @@ public class OtherPageElementsHelper extends OtherPageElements { return headerName; } - public boolean entityIsNotPresent(String entityName) { + public boolean assertEntityIsNotPresent(String entityName) { return elementIsNotPresent(getEntity(entityName)); } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/RuleChainsPageHelper.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/RuleChainsPageHelper.java index 489b7e61e3..6c822b93be 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/RuleChainsPageHelper.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/RuleChainsPageHelper.java @@ -107,7 +107,7 @@ public class RuleChainsPageHelper extends RuleChainsPageElements { return wait.until(ExpectedConditions.invisibilityOfElementLocated(By.xpath(getDeleteRuleChainFromViewBtn()))); } - public boolean ruleChainsIsNotPresent(String ruleChainName) { + public boolean assertRuleChainsIsNotPresent(String ruleChainName) { return elementsIsNotPresent(getEntity(ruleChainName)); } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/CreateAssetProfileImportTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/CreateAssetProfileImportTest.java index 8816f878e7..7c0e6dee8f 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/CreateAssetProfileImportTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/CreateAssetProfileImportTest.java @@ -98,7 +98,7 @@ public class CreateAssetProfileImportTest extends AbstractDriverBaseTest { Assert.assertNotNull(profilesPage.importingFile(EMPTY_IMPORT_MESSAGE)); Assert.assertTrue(profilesPage.importingFile(EMPTY_IMPORT_MESSAGE).isDisplayed()); - Assert.assertTrue(profilesPage.entityIsNotPresent(IMPORT_ASSET_PROFILE_NAME)); + Assert.assertTrue(profilesPage.assertEntityIsNotPresent(IMPORT_ASSET_PROFILE_NAME)); } @Epic("Asset profiles smoke") diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/DeleteAssetProfileTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/DeleteAssetProfileTest.java index 8241685a25..064aa89ff5 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/DeleteAssetProfileTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/DeleteAssetProfileTest.java @@ -55,7 +55,7 @@ public class DeleteAssetProfileTest extends AbstractDriverBaseTest { profilesPage.warningPopUpYesBtn().click(); profilesPage.refreshBtn(); - Assert.assertTrue(profilesPage.entityIsNotPresent(name)); + Assert.assertTrue(profilesPage.assertEntityIsNotPresent(name)); } @Epic("Asset profiles smoke") @@ -72,7 +72,7 @@ public class DeleteAssetProfileTest extends AbstractDriverBaseTest { profilesPage.warningPopUpYesBtn().click(); profilesPage.refreshBtn(); - Assert.assertTrue(profilesPage.entityIsNotPresent(name)); + Assert.assertTrue(profilesPage.assertEntityIsNotPresent(name)); } @Epic("Asset profiles smoke") @@ -89,7 +89,7 @@ public class DeleteAssetProfileTest extends AbstractDriverBaseTest { profilesPage.warningPopUpYesBtn().click(); profilesPage.refreshBtn(); - Assert.assertTrue(profilesPage.entityIsNotPresent(name)); + Assert.assertTrue(profilesPage.assertEntityIsNotPresent(name)); } @Epic("Asset profiles smoke") @@ -136,6 +136,6 @@ public class DeleteAssetProfileTest extends AbstractDriverBaseTest { profilesPage.deleteBtn(name).click(); profilesPage.warningPopUpYesBtn().click(); - Assert.assertTrue(profilesPage.entityIsNotPresent(name)); + Assert.assertTrue(profilesPage.assertEntityIsNotPresent(name)); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CreateCustomerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CreateCustomerTest.java index 81fcd6a020..c59ccd62f4 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CreateCustomerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CreateCustomerTest.java @@ -196,4 +196,24 @@ public class CreateCustomerTest extends AbstractDriverBaseTest { Assert.assertTrue(urlContains(urlPath), "URL contains " + urlPath); } -} \ No newline at end of file + + @Epic("Customers smoke tests") + @Feature("Create customer") + @Test(groups = "smoke") + @Description("Go to customer documentation page") + public void createCustomerAddAndRemovePhoneNumber() { + String customerName = ENTITY_NAME; + String number = "12015550123"; + + sideBarMenuView.customerBtn().click(); + customerPage.plusBtn().click(); + customerPage.titleFieldAddEntityView().sendKeys(customerName); + customerPage.phoneNumberAddEntityView().sendKeys(number); + customerPage.phoneNumberAddEntityView().sendKeys(Keys.CONTROL + "A" + Keys.BACK_SPACE); + customerPage.addBtnC().click(); + this.customerName = customerName; + customerPage.entity(customerName).click(); + + Assert.assertTrue(customerPage.phoneNumberEntityView().getAttribute("value").isEmpty(), "Phone field is empty"); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CustomerEditMenuTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CustomerEditMenuTest.java index c07e450bc4..47d950c312 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CustomerEditMenuTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CustomerEditMenuTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.msa.ui.tests.customerSmoke; import io.qameta.allure.Description; import io.qameta.allure.Epic; import io.qameta.allure.Feature; +import org.openqa.selenium.Keys; import org.testng.Assert; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeClass; @@ -307,4 +308,24 @@ public class CustomerEditMenuTest extends AbstractDriverBaseTest { Assert.assertEquals(customerPage.phoneNumberEntityView().getAttribute("value"), "+1" + number); Assert.assertEquals(customerPage.emailEntityView().getAttribute("value"), email); } -} \ No newline at end of file + + @Epic("Customers smoke tests") + @Feature("Edit customer") + @Test(groups = "smoke") + @Description("Delete phone number") + public void deletePhoneNumber() { + String customerName = ENTITY_NAME; + int number = 2015550123; + testRestClient.postCustomer(defaultCustomerPrototype(customerName, number)); + this.customerName = customerName; + + sideBarMenuView.customerBtn().click(); + customerPage.entity(customerName).click(); + customerPage.editPencilBtn().click(); + customerPage.phoneNumberEntityView().click(); + customerPage.phoneNumberEntityView().sendKeys(Keys.CONTROL + "A" + Keys.BACK_SPACE); + customerPage.doneBtnEditView().click(); + + Assert.assertTrue(customerPage.phoneNumberEntityView().getAttribute("value").isEmpty(), "Phone field is empty"); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/DeleteCustomerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/DeleteCustomerTest.java index 6421c30c31..4e6b02ace7 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/DeleteCustomerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/DeleteCustomerTest.java @@ -57,7 +57,7 @@ public class DeleteCustomerTest extends AbstractDriverBaseTest { String deletedCustomer = customerPage.deleteRuleChainTrash(customer); customerPage.refreshBtn().click(); - Assert.assertTrue(customerPage.entityIsNotPresent(deletedCustomer)); + Assert.assertTrue(customerPage.assertEntityIsNotPresent(deletedCustomer)); } @Epic("Customers smoke tests") @@ -72,7 +72,7 @@ public class DeleteCustomerTest extends AbstractDriverBaseTest { String deletedCustomer = customerPage.deleteSelected(customerName); ruleChainsPage.refreshBtn().click(); - Assert.assertTrue(ruleChainsPage.entityIsNotPresent(deletedCustomer)); + Assert.assertTrue(ruleChainsPage.assertEntityIsNotPresent(deletedCustomer)); } @Epic("Customers smoke tests") @@ -89,7 +89,7 @@ public class DeleteCustomerTest extends AbstractDriverBaseTest { customerPage.warningPopUpYesBtn().click(); jsClick(customerPage.refreshBtn()); - Assert.assertTrue(customerPage.entityIsNotPresent(customerName)); + Assert.assertTrue(customerPage.assertEntityIsNotPresent(customerName)); } @Epic("Customers smoke tests") @@ -104,6 +104,6 @@ public class DeleteCustomerTest extends AbstractDriverBaseTest { String deletedCustomer = customerPage.deleteRuleChainTrash(customer); customerPage.refreshBtn().click(); - Assert.assertTrue(customerPage.entityIsNotPresent(deletedCustomer)); + Assert.assertTrue(customerPage.assertEntityIsNotPresent(deletedCustomer)); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileImportTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileImportTest.java index 436f19c3d4..5923335a83 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileImportTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileImportTest.java @@ -98,7 +98,7 @@ public class CreateDeviceProfileImportTest extends AbstractDriverBaseTest { Assert.assertNotNull(profilesPage.importingFile(EMPTY_IMPORT_MESSAGE)); Assert.assertTrue(profilesPage.importingFile(EMPTY_IMPORT_MESSAGE).isDisplayed()); - Assert.assertTrue(profilesPage.entityIsNotPresent(IMPORT_DEVICE_PROFILE_NAME)); + Assert.assertTrue(profilesPage.assertEntityIsNotPresent(IMPORT_DEVICE_PROFILE_NAME)); } @Epic("Device profile smoke tests") diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeleteDeviceProfileTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeleteDeviceProfileTest.java index ccc97b8c94..b4161a762b 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeleteDeviceProfileTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeleteDeviceProfileTest.java @@ -55,7 +55,7 @@ public class DeleteDeviceProfileTest extends AbstractDriverBaseTest { profilesPage.warningPopUpYesBtn().click(); profilesPage.refreshBtn(); - Assert.assertTrue(profilesPage.entityIsNotPresent(name)); + Assert.assertTrue(profilesPage.assertEntityIsNotPresent(name)); } @Epic("Device profile smoke tests") @@ -72,7 +72,7 @@ public class DeleteDeviceProfileTest extends AbstractDriverBaseTest { profilesPage.warningPopUpYesBtn().click(); profilesPage.refreshBtn(); - Assert.assertTrue(profilesPage.entityIsNotPresent(name)); + Assert.assertTrue(profilesPage.assertEntityIsNotPresent(name)); } @Epic("Device profile smoke tests") @@ -89,7 +89,7 @@ public class DeleteDeviceProfileTest extends AbstractDriverBaseTest { profilesPage.warningPopUpYesBtn().click(); profilesPage.refreshBtn(); - Assert.assertTrue(profilesPage.entityIsNotPresent(name)); + Assert.assertTrue(profilesPage.assertEntityIsNotPresent(name)); } @Epic("Device profile smoke tests") @@ -135,6 +135,6 @@ public class DeleteDeviceProfileTest extends AbstractDriverBaseTest { profilesPage.deleteBtn(name).click(); profilesPage.warningPopUpYesBtn().click(); - Assert.assertTrue(profilesPage.entityIsNotPresent(name)); + Assert.assertTrue(profilesPage.assertEntityIsNotPresent(name)); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/OpenRuleChainTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/OpenRuleChainTest.java deleted file mode 100644 index 220b718ae8..0000000000 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/OpenRuleChainTest.java +++ /dev/null @@ -1,96 +0,0 @@ -/** - * 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.msa.ui.tests.ruleChainsSmoke; - -import io.qameta.allure.Description; -import io.qameta.allure.Epic; -import io.qameta.allure.Feature; -import org.testng.Assert; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.Test; -import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; -import org.thingsboard.server.msa.ui.pages.LoginPageHelper; -import org.thingsboard.server.msa.ui.pages.OpenRuleChainPageHelper; -import org.thingsboard.server.msa.ui.pages.RuleChainsPageHelper; -import org.thingsboard.server.msa.ui.pages.SideBarMenuViewElements; -import org.thingsboard.server.msa.ui.utils.EntityPrototypes; - -import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; -import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; - -public class OpenRuleChainTest extends AbstractDriverBaseTest { - - private SideBarMenuViewElements sideBarMenuView; - private RuleChainsPageHelper ruleChainsPage; - private OpenRuleChainPageHelper openRuleChainPage; - private String ruleChainName; - - @BeforeClass - public void login() { - new LoginPageHelper(driver).authorizationTenant(); - sideBarMenuView = new SideBarMenuViewElements(driver); - ruleChainsPage = new RuleChainsPageHelper(driver); - openRuleChainPage = new OpenRuleChainPageHelper(driver); - } - - @AfterMethod - public void delete() { - if (ruleChainName != null) { - testRestClient.deleteRuleChain(getRuleChainByName(ruleChainName).getId()); - ruleChainName = null; - } - } - - @Epic("Rule chains smoke tests") - @Feature("Open rule chain") - @Test(priority = 10, groups = "smoke") - @Description("Open the rule chain by clicking on its name") - public void openRuleChainByRightCornerBtn() { - String ruleChainName = ENTITY_NAME + random(); - testRestClient.postRuleChain(EntityPrototypes.defaultRuleChainPrototype(ruleChainName)); - this.ruleChainName = ruleChainName; - - sideBarMenuView.ruleChainsBtn().click(); - ruleChainsPage.entity(ruleChainName).click(); - openRuleChainPage.setHeadName(); - - Assert.assertTrue(urlContains(String.valueOf(getRuleChainByName(ruleChainName).getId()))); - Assert.assertTrue(openRuleChainPage.headRuleChainName().isDisplayed()); - Assert.assertTrue(openRuleChainPage.inputNode().isDisplayed()); - Assert.assertEquals(ruleChainName, openRuleChainPage.getHeadName()); - } - - @Epic("Rule chains smoke tests") - @Feature("Open rule chain") - @Test(priority = 10, groups = "smoke") - @Description("Open the rule chain by clicking on the 'Open rule chain' button in the entity view") - public void openRuleChainByViewBtn() { - String ruleChainName = ENTITY_NAME + random(); - testRestClient.postRuleChain(EntityPrototypes.defaultRuleChainPrototype(ruleChainName)); - this.ruleChainName = ruleChainName; - - sideBarMenuView.ruleChainsBtn().click(); - ruleChainsPage.detailsBtn(ruleChainName).click(); - ruleChainsPage.openRuleChainFromViewBtn().click(); - openRuleChainPage.setHeadName(); - - Assert.assertTrue(urlContains(String.valueOf(getRuleChainByName(ruleChainName).getId()))); - Assert.assertTrue(openRuleChainPage.headRuleChainName().isDisplayed()); - Assert.assertTrue(openRuleChainPage.inputNode().isDisplayed()); - Assert.assertEquals(ruleChainName, openRuleChainPage.getHeadName()); - } -} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/AbstractRuleChainTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/AbstractRuleChainTest.java new file mode 100644 index 0000000000..eb06858125 --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/AbstractRuleChainTest.java @@ -0,0 +1,49 @@ +/** + * 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.msa.ui.tests.rulechainssmoke; + +import io.qameta.allure.Epic; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; +import org.thingsboard.server.msa.ui.pages.LoginPageHelper; +import org.thingsboard.server.msa.ui.pages.OpenRuleChainPageHelper; +import org.thingsboard.server.msa.ui.pages.RuleChainsPageHelper; +import org.thingsboard.server.msa.ui.pages.SideBarMenuViewElements; + +@Epic("Rule chains smoke tests") +abstract public class AbstractRuleChainTest extends AbstractDriverBaseTest { + + protected SideBarMenuViewElements sideBarMenuView; + protected RuleChainsPageHelper ruleChainsPage; + protected OpenRuleChainPageHelper openRuleChainPage; + protected String ruleChainName; + + @BeforeClass + public void login() { + new LoginPageHelper(driver).authorizationTenant(); + sideBarMenuView = new SideBarMenuViewElements(driver); + ruleChainsPage = new RuleChainsPageHelper(driver); + openRuleChainPage = new OpenRuleChainPageHelper(driver); + } + + @AfterMethod + public void delete() { + deleteRuleChainByName(ruleChainName); + ruleChainName = null; + } +} + diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/CreateRuleChainImportTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/CreateRuleChainImportTest.java similarity index 53% rename from msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/CreateRuleChainImportTest.java rename to msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/CreateRuleChainImportTest.java index dd2f8a5895..53ec75697e 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/CreateRuleChainImportTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/CreateRuleChainImportTest.java @@ -13,56 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; +package org.thingsboard.server.msa.ui.tests.rulechainssmoke; import io.qameta.allure.Description; -import io.qameta.allure.Epic; import io.qameta.allure.Feature; import org.openqa.selenium.WebElement; -import org.testng.Assert; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; -import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; -import org.thingsboard.server.msa.ui.pages.LoginPageHelper; -import org.thingsboard.server.msa.ui.pages.OpenRuleChainPageHelper; -import org.thingsboard.server.msa.ui.pages.RuleChainsPageHelper; -import org.thingsboard.server.msa.ui.pages.SideBarMenuViewElements; - -import java.util.ArrayList; +import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.msa.ui.utils.Const.EMPTY_IMPORT_MESSAGE; import static org.thingsboard.server.msa.ui.utils.Const.IMPORT_RULE_CHAIN_FILE_NAME; import static org.thingsboard.server.msa.ui.utils.Const.IMPORT_RULE_CHAIN_NAME; import static org.thingsboard.server.msa.ui.utils.Const.IMPORT_TXT_FILE_NAME; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultRuleChainPrototype; -public class CreateRuleChainImportTest extends AbstractDriverBaseTest { - private SideBarMenuViewElements sideBarMenuView; - private RuleChainsPageHelper ruleChainsPage; - private OpenRuleChainPageHelper openRuleChainPage; +@Feature("Import rule chain") +public class CreateRuleChainImportTest extends AbstractRuleChainTest { + private final String absolutePathToFileImportRuleChain = getClass().getClassLoader().getResource(IMPORT_RULE_CHAIN_FILE_NAME).getPath(); private final String absolutePathToFileImportTxt = getClass().getClassLoader().getResource(IMPORT_TXT_FILE_NAME).getPath(); - private String ruleChainName; - @BeforeClass - public void login() { - new LoginPageHelper(driver).authorizationTenant(); - sideBarMenuView = new SideBarMenuViewElements(driver); - ruleChainsPage = new RuleChainsPageHelper(driver); - openRuleChainPage = new OpenRuleChainPageHelper(driver); - } - - @AfterMethod - public void delete() { - if (ruleChainName != null) { - testRestClient.deleteRuleChain(getRuleChainByName(ruleChainName).getId()); - ruleChainName = null; - } - } - - @Epic("Rule chains smoke tests") - @Feature("Import rule chain") @Test(priority = 10, groups = "smoke") @Description("Drop json file") public void importRuleChain() { @@ -70,12 +40,9 @@ public class CreateRuleChainImportTest extends AbstractDriverBaseTest { ruleChainsPage.openImportRuleChainView(); ruleChainsPage.browseFile().sendKeys(absolutePathToFileImportRuleChain); - Assert.assertNotNull(ruleChainsPage.importingFile(IMPORT_RULE_CHAIN_FILE_NAME)); - Assert.assertTrue(ruleChainsPage.importingFile(IMPORT_RULE_CHAIN_FILE_NAME).isDisplayed()); + assertIsDisplayed(ruleChainsPage.importingFile(IMPORT_RULE_CHAIN_FILE_NAME)); } - @Epic("Rule chains smoke tests") - @Feature("Import rule chain") @Test(priority = 20, groups = "smoke") @Description("Drop json file and delete it") public void importRuleChainAndDeleteFile() { @@ -84,13 +51,10 @@ public class CreateRuleChainImportTest extends AbstractDriverBaseTest { ruleChainsPage.browseFile().sendKeys(absolutePathToFileImportRuleChain); ruleChainsPage.clearImportFileBtn().click(); - Assert.assertNotNull(ruleChainsPage.importingFile(EMPTY_IMPORT_MESSAGE)); - Assert.assertTrue(ruleChainsPage.importingFile(EMPTY_IMPORT_MESSAGE).isDisplayed()); - Assert.assertTrue(ruleChainsPage.entityIsNotPresent(IMPORT_RULE_CHAIN_FILE_NAME)); + assertIsDisplayed(ruleChainsPage.importingFile(EMPTY_IMPORT_MESSAGE)); + ruleChainsPage.assertEntityIsNotPresent(IMPORT_RULE_CHAIN_FILE_NAME); } - @Epic("Rule chains smoke tests") - @Feature("Import rule chain") @Test(priority = 20, groups = "smoke") @Description("Import txt file") public void importTxtFile() { @@ -98,12 +62,9 @@ public class CreateRuleChainImportTest extends AbstractDriverBaseTest { ruleChainsPage.openImportRuleChainView(); ruleChainsPage.browseFile().sendKeys(absolutePathToFileImportTxt); - Assert.assertNotNull(ruleChainsPage.importingFile(EMPTY_IMPORT_MESSAGE)); - Assert.assertTrue(ruleChainsPage.importingFile(EMPTY_IMPORT_MESSAGE).isDisplayed()); + assertIsDisplayed(ruleChainsPage.importingFile(EMPTY_IMPORT_MESSAGE)); } - @Epic("Rule chains smoke tests") - @Feature("Import rule chain") @Test(priority = 30, groups = "smoke") @Description("Import rule chain") public void importRuleChainAndSave() { @@ -116,18 +77,14 @@ public class CreateRuleChainImportTest extends AbstractDriverBaseTest { ruleChainName = IMPORT_RULE_CHAIN_NAME; sideBarMenuView.ruleChainsBtn().click(); - Assert.assertNotNull(ruleChainsPage.entity(IMPORT_RULE_CHAIN_NAME)); - Assert.assertTrue(ruleChainsPage.entity(IMPORT_RULE_CHAIN_NAME).isDisplayed()); + assertIsDisplayed(ruleChainsPage.entity(ruleChainName)); } - @Epic("Rule chains smoke tests") - @Feature("Import rule chain") @Test(priority = 40, groups = "smoke") @Description("Import rule chain with same name") public void importRuleChainAndSaveWithSameName() { - String ruleChainName = IMPORT_RULE_CHAIN_NAME; + ruleChainName = IMPORT_RULE_CHAIN_NAME; testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName)); - this.ruleChainName = ruleChainName; sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.openImportRuleChainView(); @@ -137,15 +94,8 @@ public class CreateRuleChainImportTest extends AbstractDriverBaseTest { doneBtn.click(); sideBarMenuView.ruleChainsBtn().click(); - boolean entityNotNull = ruleChainsPage.entity(ruleChainName) != null; - boolean entitiesSizeMoreOne = ruleChainsPage.entities(ruleChainName).size() > 1; - ArrayList entityIsDisplayed = new ArrayList<>(); - ruleChainsPage.entities(ruleChainName).forEach(x -> entityIsDisplayed.add(x.isDisplayed())); - - testRestClient.deleteRuleChain(getRuleChainByName(ruleChainName).getId()); - - Assert.assertTrue(entityNotNull); - Assert.assertTrue(entitiesSizeMoreOne); - entityIsDisplayed.forEach(Assert::assertTrue); + assertThat(ruleChainsPage.entities(ruleChainName).size() > 1). + as("More than 1 rule chains have been created").isTrue(); + ruleChainsPage.entities(ruleChainName).forEach(this::assertIsDisplayed); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/CreateRuleChainTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/CreateRuleChainTest.java similarity index 53% rename from msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/CreateRuleChainTest.java rename to msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/CreateRuleChainTest.java index e8bdaff9da..f19fa7ccca 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/CreateRuleChainTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/CreateRuleChainTest.java @@ -13,54 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; +package org.thingsboard.server.msa.ui.tests.rulechainssmoke; import io.qameta.allure.Description; -import io.qameta.allure.Epic; import io.qameta.allure.Feature; -import org.testng.Assert; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; -import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; -import org.thingsboard.server.msa.ui.pages.LoginPageHelper; -import org.thingsboard.server.msa.ui.pages.RuleChainsPageHelper; -import org.thingsboard.server.msa.ui.pages.SideBarMenuViewElements; import org.thingsboard.server.msa.ui.utils.EntityPrototypes; -import java.util.ArrayList; - +import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; import static org.thingsboard.server.msa.ui.utils.Const.EMPTY_RULE_CHAIN_MESSAGE; import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; -public class CreateRuleChainTest extends AbstractDriverBaseTest { - - private SideBarMenuViewElements sideBarMenuView; - private RuleChainsPageHelper ruleChainsPage; - private String ruleChainName; - - @BeforeClass - public void login() { - new LoginPageHelper(driver).authorizationTenant(); - sideBarMenuView = new SideBarMenuViewElements(driver); - ruleChainsPage = new RuleChainsPageHelper(driver); - } - - @AfterMethod - public void delete() { - if (ruleChainName != null) { - testRestClient.deleteRuleChain(getRuleChainByName(ruleChainName).getId()); - ruleChainName = null; - } - } +@Feature("Create rule chain") +public class CreateRuleChainTest extends AbstractRuleChainTest { - @Epic("Rule chains smoke tests") - @Feature("Create rule chain") @Test(priority = 10, groups = "smoke") @Description("Add rule chain after specifying the name (text/numbers /special characters)") public void createRuleChain() { - String ruleChainName = ENTITY_NAME + random(); + ruleChainName = ENTITY_NAME + random(); sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.openCreateRuleChainView(); @@ -68,18 +39,14 @@ public class CreateRuleChainTest extends AbstractDriverBaseTest { ruleChainsPage.nameField().sendKeys(ruleChainName); ruleChainsPage.addBtnC().click(); ruleChainsPage.refreshBtn().click(); - this.ruleChainName = ruleChainName; - Assert.assertNotNull(ruleChainsPage.entity(ruleChainName)); - Assert.assertTrue(ruleChainsPage.entity(ruleChainName).isDisplayed()); + assertIsDisplayed(ruleChainsPage.entity(ruleChainName)); } - @Epic("Rule chains smoke tests") - @Feature("Create rule chain") @Test(priority = 10, groups = "smoke") @Description("Add rule chain after specifying the name and description (text/numbers /special characters)") public void createRuleChainWithDescription() { - String ruleChainName = ENTITY_NAME + random(); + ruleChainName = ENTITY_NAME + random(); sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.openCreateRuleChainView(); @@ -87,27 +54,23 @@ public class CreateRuleChainTest extends AbstractDriverBaseTest { ruleChainsPage.descriptionAddEntityView().sendKeys(ruleChainName); ruleChainsPage.addBtnC().click(); ruleChainsPage.refreshBtn().click(); - this.ruleChainName = ruleChainName; ruleChainsPage.detailsBtn(ruleChainName).click(); ruleChainsPage.setHeaderName(); - Assert.assertEquals(ruleChainsPage.getHeaderName(), ruleChainName); - Assert.assertEquals(ruleChainsPage.descriptionEntityView().getAttribute("value"), ruleChainName); + assertThat(ruleChainsPage.getHeaderName()).as("Header of rule chain details tab").isEqualTo(ruleChainName); + assertThat(ruleChainsPage.descriptionEntityView().getAttribute("value")) + .as("Description in rule chain details tab").isEqualTo(ruleChainName); } - @Epic("Rule chains smoke tests") - @Feature("Create rule chain") @Test(priority = 20, groups = "smoke") @Description("Add rule chain without the name") public void createRuleChainWithoutName() { sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.openCreateRuleChainView(); - Assert.assertFalse(ruleChainsPage.addBtnV().isEnabled()); + assertIsDisable(ruleChainsPage.addBtnV()); } - @Epic("Rule chains smoke tests") - @Feature("Create rule chain") @Test(priority = 20, groups = "smoke") @Description("Create rule chain only with spase in name") public void createRuleChainWithOnlySpace() { @@ -116,21 +79,16 @@ public class CreateRuleChainTest extends AbstractDriverBaseTest { ruleChainsPage.nameField().sendKeys(" "); ruleChainsPage.addBtnC().click(); - Assert.assertNotNull(ruleChainsPage.warningMessage()); - Assert.assertTrue(ruleChainsPage.warningMessage().isDisplayed()); - Assert.assertEquals(ruleChainsPage.warningMessage().getText(), EMPTY_RULE_CHAIN_MESSAGE); - Assert.assertNotNull(ruleChainsPage.addEntityView()); - Assert.assertTrue(ruleChainsPage.addEntityView().isDisplayed()); + assertIsDisplayed(ruleChainsPage.warningMessage()); + assertThat(ruleChainsPage.warningMessage().getText()).as("Text of warning message").isEqualTo(EMPTY_RULE_CHAIN_MESSAGE); + assertIsDisplayed(ruleChainsPage.addEntityView()); } - @Epic("Rule chains smoke tests") - @Feature("Create rule chain") @Test(priority = 20, groups = "smoke") @Description("Create a rule chain with the same name") public void createRuleChainWithSameName() { - String ruleChainName = ENTITY_NAME + random(); + ruleChainName = ENTITY_NAME + random(); testRestClient.postRuleChain(EntityPrototypes.defaultRuleChainPrototype(ruleChainName)); - this.ruleChainName = ruleChainName; sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.openCreateRuleChainView(); @@ -138,37 +96,24 @@ public class CreateRuleChainTest extends AbstractDriverBaseTest { ruleChainsPage.addBtnC().click(); ruleChainsPage.refreshBtn().click(); - boolean entityNotNull = ruleChainsPage.entity(ruleChainName) != null; - boolean entitiesSizeMoreOne = ruleChainsPage.entities(ruleChainName).size() > 1; - ArrayList entityIsDisplayed = new ArrayList<>(); - ruleChainsPage.entities(ruleChainName).forEach(x -> entityIsDisplayed.add(x.isDisplayed())); - - testRestClient.deleteRuleChain(getRuleChainByName(ruleChainName).getId()); - - Assert.assertTrue(entityNotNull); - Assert.assertTrue(entitiesSizeMoreOne); - entityIsDisplayed.forEach(Assert::assertTrue); + assertThat(ruleChainsPage.entities(ruleChainName).size() > 1). + as("More than 1 rule chains have been created").isTrue(); + ruleChainsPage.entities(ruleChainName).forEach(this::assertIsDisplayed); } - @Epic("Rule chains smoke tests") - @Feature("Create rule chain") @Test(priority = 30, groups = "smoke") @Description("Add rule chain after specifying the name (text/numbers /special characters) without refresh") public void createRuleChainWithoutRefresh() { - String ruleChainName = ENTITY_NAME + random(); + ruleChainName = ENTITY_NAME + random(); sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.openCreateRuleChainView(); ruleChainsPage.nameField().sendKeys(ruleChainName); ruleChainsPage.addBtnC().click(); - this.ruleChainName = ruleChainName; - Assert.assertNotNull(ruleChainsPage.entity(ruleChainName)); - Assert.assertTrue(ruleChainsPage.entity(ruleChainName).isDisplayed()); + assertIsDisplayed(ruleChainsPage.entity(ruleChainName)); } - @Epic("Rule chains smoke tests") - @Feature("Create rule chain") @Test(priority = 40, groups = "smoke") @Description("Go to rule chain documentation page") public void documentation() { @@ -179,6 +124,6 @@ public class CreateRuleChainTest extends AbstractDriverBaseTest { ruleChainsPage.detailsBtn(ruleChainsPage.getRuleChainName()).click(); ruleChainsPage.goToHelpPage(); - Assert.assertTrue(urlContains(urlPath), "URL contains " + urlPath); + assertThat(urlContains(urlPath)).as("Redirected URL contains " + urlPath).isTrue(); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/DeleteRuleChainTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/DeleteRuleChainTest.java similarity index 65% rename from msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/DeleteRuleChainTest.java rename to msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/DeleteRuleChainTest.java index e1f27d7bab..b5eb8eaed5 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/DeleteRuleChainTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/DeleteRuleChainTest.java @@ -13,72 +13,53 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; +package org.thingsboard.server.msa.ui.tests.rulechainssmoke; import io.qameta.allure.Description; import io.qameta.allure.Epic; import io.qameta.allure.Feature; -import org.testng.Assert; -import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; -import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; -import org.thingsboard.server.msa.ui.pages.LoginPageHelper; -import org.thingsboard.server.msa.ui.pages.RuleChainsPageHelper; -import org.thingsboard.server.msa.ui.pages.SideBarMenuViewElements; +import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; import static org.thingsboard.server.msa.ui.utils.Const.DELETE_RULE_CHAIN_WITH_PROFILE_MESSAGE; import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; import static org.thingsboard.server.msa.ui.utils.Const.ROOT_RULE_CHAIN_NAME; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultRuleChainPrototype; -public class DeleteRuleChainTest extends AbstractDriverBaseTest { - private SideBarMenuViewElements sideBarMenuView; - private RuleChainsPageHelper ruleChainsPage; +@Feature("Delete rule chain") +public class DeleteRuleChainTest extends AbstractRuleChainTest { - @BeforeClass - public void login() { - new LoginPageHelper(driver).authorizationTenant(); - sideBarMenuView = new SideBarMenuViewElements(driver); - ruleChainsPage = new RuleChainsPageHelper(driver); - } - - @Epic("Rule chains smoke tests") - @Feature("Delete rule chain") @Test(priority = 10, groups = "smoke") @Description("Remove the rule chain by clicking on the trash icon in the right side of rule chain") public void removeRuleChainByRightSideBtn() { - String ruleChainName = ENTITY_NAME + random(); + ruleChainName = ENTITY_NAME + random(); testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName)); sideBarMenuView.ruleChainsBtn().click(); String deletedRuleChain = ruleChainsPage.deleteRuleChainTrash(ruleChainName); ruleChainsPage.refreshBtn().click(); - Assert.assertTrue(ruleChainsPage.entityIsNotPresent(deletedRuleChain)); + ruleChainsPage.assertEntityIsNotPresent(deletedRuleChain); } - @Epic("Rule chains smoke tests") - @Feature("Delete rule chain") @Test(priority = 20, groups = "smoke") @Description("Remove rule chain by mark in the checkbox and then click on the trash can icon in the menu that appears at the top") public void removeSelectedRuleChain() { - String ruleChainName = ENTITY_NAME + random(); + ruleChainName = ENTITY_NAME + random(); testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName)); sideBarMenuView.ruleChainsBtn().click(); String deletedRuleChain = ruleChainsPage.deleteSelected(ruleChainName); ruleChainsPage.refreshBtn().click(); - Assert.assertTrue(ruleChainsPage.entityIsNotPresent(deletedRuleChain)); + ruleChainsPage.assertEntityIsNotPresent(deletedRuleChain); } - @Epic("Rule chains smoke tests") - @Feature("Delete rule chain") @Test(priority = 20, groups = "smoke") @Description("Remove the rule chain by clicking on the 'Delete rule chain' btn in the entity view") public void removeFromRuleChainView() { - String ruleChainName = ENTITY_NAME + random(); + ruleChainName = ENTITY_NAME + random(); testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName)); sideBarMenuView.ruleChainsBtn().click(); @@ -86,21 +67,17 @@ public class DeleteRuleChainTest extends AbstractDriverBaseTest { String deletedRuleChain = ruleChainsPage.deleteRuleChainFromView(ruleChainName); jsClick(ruleChainsPage.refreshBtn()); - Assert.assertTrue(ruleChainsPage.entityIsNotPresent(deletedRuleChain)); + ruleChainsPage.assertEntityIsNotPresent(deletedRuleChain); } - @Epic("Rule chains smoke tests") - @Feature("Delete rule chain") @Test(priority = 20, groups = "smoke") @Description("Remove the root rule chain by clicking on the trash icon in the right side of rule chain") public void removeRootRuleChain() { sideBarMenuView.ruleChainsBtn().click(); - Assert.assertFalse(ruleChainsPage.deleteBtn(ROOT_RULE_CHAIN_NAME).isEnabled()); + assertIsDisable(ruleChainsPage.deleteBtn(ROOT_RULE_CHAIN_NAME)); } - @Epic("Rule chains smoke tests") - @Feature("Delete rule chain") @Test(priority = 20, groups = "smoke") @Description("Remove root rule chain by mark in the checkbox and then click on the trash can icon in the menu that appears at the top") public void removeSelectedRootRuleChain() { @@ -117,7 +94,8 @@ public class DeleteRuleChainTest extends AbstractDriverBaseTest { sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.detailsBtn(ROOT_RULE_CHAIN_NAME).click(); - Assert.assertTrue(ruleChainsPage.deleteBtnInRootRuleChainIsNotDisplayed()); + assertThat(ruleChainsPage.deleteBtnInRootRuleChainIsNotDisplayed()) + .as("Delete btn isn't displayed in details tab").isTrue(); } @Epic("Rule chains smoke tests") @@ -132,15 +110,12 @@ public class DeleteRuleChainTest extends AbstractDriverBaseTest { ruleChainsPage.warningPopUpYesBtn().click(); ruleChainsPage.refreshBtn().click(); - Assert.assertNotNull(ruleChainsPage.entity(deletedRuleChain)); - Assert.assertTrue(ruleChainsPage.entity(deletedRuleChain).isDisplayed()); - Assert.assertNotNull(ruleChainsPage.warningMessage()); - Assert.assertTrue(ruleChainsPage.warningMessage().isDisplayed()); - Assert.assertEquals(ruleChainsPage.warningMessage().getText(), DELETE_RULE_CHAIN_WITH_PROFILE_MESSAGE); + assertIsDisplayed(ruleChainsPage.entity(deletedRuleChain)); + assertIsDisplayed(ruleChainsPage.warningMessage()); + assertThat(ruleChainsPage.warningMessage().getText()) + .as("Text of warning message").isEqualTo(DELETE_RULE_CHAIN_WITH_PROFILE_MESSAGE); } - @Epic("Rule chains smoke tests") - @Feature("Delete rule chain") @Test(priority = 20, groups = "smoke") @Description("Remove the rule chain with device profile by mark in the checkbox and then click on the trash can icon in the menu that appears at the top") public void removeSelectedProfileRuleChain() { @@ -148,11 +123,10 @@ public class DeleteRuleChainTest extends AbstractDriverBaseTest { String deletedRuleChain = ruleChainsPage.deleteSelected("Thermostat"); ruleChainsPage.refreshBtn().click(); - Assert.assertNotNull(ruleChainsPage.entity(deletedRuleChain)); - Assert.assertTrue(ruleChainsPage.entity(deletedRuleChain).isDisplayed()); - Assert.assertNotNull(ruleChainsPage.warningMessage()); - Assert.assertTrue(ruleChainsPage.warningMessage().isDisplayed()); - Assert.assertEquals(ruleChainsPage.warningMessage().getText(), DELETE_RULE_CHAIN_WITH_PROFILE_MESSAGE); + assertIsDisplayed(ruleChainsPage.entity(deletedRuleChain)); + assertIsDisplayed(ruleChainsPage.warningMessage()); + assertThat(ruleChainsPage.warningMessage().getText()) + .as("Text of warning message").isEqualTo(DELETE_RULE_CHAIN_WITH_PROFILE_MESSAGE); } @Epic("Rule chains smoke tests") @@ -167,10 +141,10 @@ public class DeleteRuleChainTest extends AbstractDriverBaseTest { jsClick(ruleChainsPage.deleteBtnFromView()); ruleChainsPage.warningPopUpYesBtn().click(); - Assert.assertNotNull(ruleChainsPage.entity(deletedRuleChain)); - Assert.assertNotNull(ruleChainsPage.warningMessage()); - Assert.assertTrue(ruleChainsPage.warningMessage().isDisplayed()); - Assert.assertEquals(ruleChainsPage.warningMessage().getText(), DELETE_RULE_CHAIN_WITH_PROFILE_MESSAGE); + assertIsDisplayed(ruleChainsPage.entity(deletedRuleChain)); + assertIsDisplayed(ruleChainsPage.warningMessage()); + assertThat(ruleChainsPage.warningMessage().getText()) + .as("Text of warning message").isEqualTo(DELETE_RULE_CHAIN_WITH_PROFILE_MESSAGE); } @Epic("Rule chains smoke tests") @@ -178,12 +152,12 @@ public class DeleteRuleChainTest extends AbstractDriverBaseTest { @Test(priority = 30, groups = "smoke") @Description("Remove the rule chain by clicking on the trash icon in the right side of rule chain without refresh") public void removeRuleChainByRightSideBtnWithoutRefresh() { - String ruleChainName = ENTITY_NAME + random(); + ruleChainName = ENTITY_NAME + random(); testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName)); sideBarMenuView.ruleChainsBtn().click(); String deletedRuleChain = ruleChainsPage.deleteRuleChainTrash(ruleChainName); - Assert.assertTrue(ruleChainsPage.entityIsNotPresent(deletedRuleChain)); + ruleChainsPage.assertEntityIsNotPresent(deletedRuleChain); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/DeleteSeveralRuleChainsTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/DeleteSeveralRuleChainsTest.java similarity index 69% rename from msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/DeleteSeveralRuleChainsTest.java rename to msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/DeleteSeveralRuleChainsTest.java index 068fbbf71b..192bb6fe59 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/DeleteSeveralRuleChainsTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/DeleteSeveralRuleChainsTest.java @@ -13,43 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; +package org.thingsboard.server.msa.ui.tests.rulechainssmoke; import io.qameta.allure.Description; -import io.qameta.allure.Epic; import io.qameta.allure.Feature; -import org.testng.Assert; -import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; -import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; -import org.thingsboard.server.msa.ui.pages.LoginPageHelper; -import org.thingsboard.server.msa.ui.pages.RuleChainsPageHelper; -import org.thingsboard.server.msa.ui.pages.SideBarMenuViewElements; import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; import static org.thingsboard.server.msa.ui.utils.Const.ROOT_RULE_CHAIN_NAME; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultRuleChainPrototype; -public class DeleteSeveralRuleChainsTest extends AbstractDriverBaseTest { +@Feature("Delete several rule chains") +public class DeleteSeveralRuleChainsTest extends AbstractRuleChainTest { - private SideBarMenuViewElements sideBarMenuView; - private RuleChainsPageHelper ruleChainsPage; - - @BeforeClass - public void login() { - new LoginPageHelper(driver).authorizationTenant(); - sideBarMenuView = new SideBarMenuViewElements(driver); - ruleChainsPage = new RuleChainsPageHelper(driver); - } - - @Epic("Rule chains smoke tests") - @Feature("Delete several rule chains") @Test(priority = 10, groups = "smoke") @Description("Remove several rule chains by mark in the checkbox and then click on the trash can icon in the menu " + "that appears at the top") public void canDeleteSeveralRuleChainsByTopBtn() { - String ruleChainName = ENTITY_NAME + random(); + ruleChainName = ENTITY_NAME + random(); testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName + 1)); testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName)); @@ -57,16 +39,14 @@ public class DeleteSeveralRuleChainsTest extends AbstractDriverBaseTest { ruleChainsPage.deleteSelected(2); ruleChainsPage.refreshBtn().click(); - Assert.assertTrue(ruleChainsPage.ruleChainsIsNotPresent(ruleChainName)); + ruleChainsPage.assertRuleChainsIsNotPresent(ruleChainName); } - @Epic("Rule chains smoke tests") - @Feature("Delete several rule chains") @Test(priority = 10, groups = "smoke") @Description("Remove several rule chains by mark all the rule chains on the page by clicking in the topmost checkbox" + " and then clicking on the trash icon in the menu that appears") public void selectAllRuleChain() { - String ruleChainName = ENTITY_NAME + random(); + ruleChainName = ENTITY_NAME + random(); testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName + 1)); testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName)); @@ -76,11 +56,9 @@ public class DeleteSeveralRuleChainsTest extends AbstractDriverBaseTest { ruleChainsPage.warningPopUpYesBtn().click(); ruleChainsPage.refreshBtn().click(); - Assert.assertTrue(ruleChainsPage.ruleChainsIsNotPresent(ruleChainName)); + ruleChainsPage.assertRuleChainsIsNotPresent(ruleChainName); } - @Epic("Rule chains smoke tests") - @Feature("Delete several rule chains") @Test(priority = 20, groups = "smoke") @Description("Remove the root rule chain by mark in the checkbox and then click on the trash can icon in the menu " + "that appears at the top") @@ -88,11 +66,9 @@ public class DeleteSeveralRuleChainsTest extends AbstractDriverBaseTest { sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.selectAllCheckBox().click(); - Assert.assertFalse(ruleChainsPage.deleteBtn(ROOT_RULE_CHAIN_NAME).isEnabled()); + assertIsDisable(ruleChainsPage.deleteBtn(ROOT_RULE_CHAIN_NAME)); } - @Epic("Rule chains smoke tests") - @Feature("Delete several rule chains") @Test(priority = 20, groups = "smoke") @Description("Remove the root rule chain by mark all the rule chains on the page by clicking in the topmost checkbox" + " and then clicking on the trash icon in the menu that appears") @@ -103,8 +79,6 @@ public class DeleteSeveralRuleChainsTest extends AbstractDriverBaseTest { ruleChainsPage.assertCheckBoxIsNotDisplayed(ROOT_RULE_CHAIN_NAME); } - @Epic("Rule chains smoke tests") - @Feature("Delete several rule chains") @Test(priority = 30, groups = "smoke") @Description("Remove several rule chains by mark in the checkbox and then click on the trash can icon in the menu " + "that appears at the top without refresh") @@ -116,6 +90,6 @@ public class DeleteSeveralRuleChainsTest extends AbstractDriverBaseTest { sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.deleteSelected(2); - Assert.assertTrue(ruleChainsPage.ruleChainsIsNotPresent(ruleChainName)); + ruleChainsPage.assertRuleChainsIsNotPresent(ruleChainName); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/MakeRuleChainRootTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/MakeRuleChainRootTest.java similarity index 63% rename from msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/MakeRuleChainRootTest.java rename to msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/MakeRuleChainRootTest.java index cf252ea22a..809e6c02fb 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/MakeRuleChainRootTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/MakeRuleChainRootTest.java @@ -13,39 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; +package org.thingsboard.server.msa.ui.tests.rulechainssmoke; import io.qameta.allure.Description; -import io.qameta.allure.Epic; import io.qameta.allure.Feature; -import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; -import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; -import org.thingsboard.server.msa.ui.pages.LoginPageHelper; -import org.thingsboard.server.msa.ui.pages.RuleChainsPageHelper; -import org.thingsboard.server.msa.ui.pages.SideBarMenuViewElements; -public class MakeRuleChainRootTest extends AbstractDriverBaseTest { +import static org.assertj.core.api.Assertions.assertThat; - private SideBarMenuViewElements sideBarMenuView; - private RuleChainsPageHelper ruleChainsPage; - - @BeforeClass - public void login() { - new LoginPageHelper(driver).authorizationTenant(); - sideBarMenuView = new SideBarMenuViewElements(driver); - ruleChainsPage = new RuleChainsPageHelper(driver); - } +@Feature("Make rule chain root") +public class MakeRuleChainRootTest extends AbstractRuleChainTest { @AfterMethod public void makeRoot() { - testRestClient.setRootRuleChain(getRuleChainByName("Root Rule Chain").getId()); + setRootRuleChain("Root Rule Chain"); } - @Epic("Rule chains smoke tests") - @Feature("Make rule chain root") @Test(priority = 10, groups = "smoke") @Description("Make rule chain root by clicking on the 'Make rule chain root' icon in the right corner") public void makeRuleChainRootByRightCornerBtn() { @@ -55,11 +39,9 @@ public class MakeRuleChainRootTest extends AbstractDriverBaseTest { ruleChainsPage.makeRootBtn(ruleChain).click(); ruleChainsPage.warningPopUpYesBtn().click(); - Assert.assertTrue(ruleChainsPage.rootCheckBoxEnable(ruleChain).isDisplayed()); + assertIsDisplayed(ruleChainsPage.rootCheckBoxEnable(ruleChain)); } - @Epic("Rule chains smoke tests") - @Feature("Make rule chain root") @Test(priority = 20, groups = "smoke") @Description("Make rule chain root by clicking on the 'Make rule chain root' button in the entity view") public void makeRuleChainRootFromView() { @@ -71,11 +53,9 @@ public class MakeRuleChainRootTest extends AbstractDriverBaseTest { ruleChainsPage.warningPopUpYesBtn().click(); ruleChainsPage.closeEntityViewBtn().click(); - Assert.assertTrue(ruleChainsPage.rootCheckBoxEnable(ruleChain).isDisplayed()); + assertIsDisplayed(ruleChainsPage.rootCheckBoxEnable(ruleChain)); } - @Epic("Rule chains smoke tests") - @Feature("Make rule chain root") @Test(priority = 30, groups = "smoke") @Description("Make multiple root rule chains (only one rule chain can be root)") public void multiplyRoot() { @@ -87,6 +67,6 @@ public class MakeRuleChainRootTest extends AbstractDriverBaseTest { ruleChainsPage.warningPopUpYesBtn().click(); ruleChainsPage.closeEntityViewBtn().click(); - Assert.assertEquals(ruleChainsPage.rootCheckBoxesEnable().size(), 1); + assertThat(ruleChainsPage.rootCheckBoxesEnable()).as("Enable only 1 root checkbox").hasSize(1); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/OpenRuleChainTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/OpenRuleChainTest.java new file mode 100644 index 0000000000..8602fe982e --- /dev/null +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/OpenRuleChainTest.java @@ -0,0 +1,66 @@ +/** + * 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.msa.ui.tests.rulechainssmoke; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import org.testng.annotations.Test; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.msa.ui.utils.EntityPrototypes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; +import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; + +@Feature("Open rule chain") +public class OpenRuleChainTest extends AbstractRuleChainTest { + + @Test(priority = 10, groups = "smoke") + @Description("Open the rule chain by clicking on its name") + public void openRuleChainByRightCornerBtn() { + ruleChainName = ENTITY_NAME + random(); + testRestClient.postRuleChain(EntityPrototypes.defaultRuleChainPrototype(ruleChainName)); + RuleChain ruleChain = getRuleChainByName(ruleChainName); + + sideBarMenuView.ruleChainsBtn().click(); + ruleChainsPage.entity(ruleChainName).click(); + openRuleChainPage.setHeadName(); + + assertThat(urlContains(ruleChain.getUuidId().toString())).as("URL contains rule chain's ID").isTrue(); + assertIsDisplayed(openRuleChainPage.headRuleChainName()); + assertIsDisplayed(openRuleChainPage.inputNode()); + assertThat(openRuleChainPage.getHeadName()).as("Head of opened rule chain page text").isEqualTo(ruleChainName); + } + + @Test(priority = 10, groups = "smoke") + @Description("Open the rule chain by clicking on the 'Open rule chain' button in the entity view") + public void openRuleChainByViewBtn() { + ruleChainName = ENTITY_NAME + random(); + testRestClient.postRuleChain(EntityPrototypes.defaultRuleChainPrototype(ruleChainName)); + RuleChain ruleChain = getRuleChainByName(ruleChainName); + + sideBarMenuView.ruleChainsBtn().click(); + ruleChainsPage.detailsBtn(ruleChainName).click(); + ruleChainsPage.openRuleChainFromViewBtn().click(); + openRuleChainPage.setHeadName(); + + assertThat(ruleChain).as("Rule chain created").isNotNull(); + assertThat(urlContains(ruleChain.getUuidId().toString())).as("URL contains rule chain's ID").isTrue(); + assertIsDisplayed(openRuleChainPage.headRuleChainName()); + assertIsDisplayed(openRuleChainPage.inputNode()); + assertThat(openRuleChainPage.getHeadName()).as("Head of opened rule chain page text").isEqualTo(ruleChainName); + } +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/RuleChainEditMenuTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/RuleChainEditMenuTest.java similarity index 56% rename from msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/RuleChainEditMenuTest.java rename to msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/RuleChainEditMenuTest.java index 3c65bc8f0a..eaeaa6e61f 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/RuleChainEditMenuTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/RuleChainEditMenuTest.java @@ -13,58 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; +package org.thingsboard.server.msa.ui.tests.rulechainssmoke; import io.qameta.allure.Description; -import io.qameta.allure.Epic; import io.qameta.allure.Feature; -import org.testng.Assert; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; -import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; -import org.thingsboard.server.msa.ui.pages.LoginPageHelper; -import org.thingsboard.server.msa.ui.pages.RuleChainsPageHelper; -import org.thingsboard.server.msa.ui.pages.SideBarMenuViewElements; import org.thingsboard.server.msa.ui.utils.DataProviderCredential; import org.thingsboard.server.msa.ui.utils.EntityPrototypes; +import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.msa.ui.base.AbstractBasePage.getRandomNumber; import static org.thingsboard.server.msa.ui.base.AbstractBasePage.random; import static org.thingsboard.server.msa.ui.utils.Const.EMPTY_RULE_CHAIN_MESSAGE; import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultRuleChainPrototype; -public class RuleChainEditMenuTest extends AbstractDriverBaseTest { +@Feature("Edit rule chain") +public class RuleChainEditMenuTest extends AbstractRuleChainTest { - private SideBarMenuViewElements sideBarMenuView; - private RuleChainsPageHelper ruleChainsPage; - private String ruleChainName; - - @BeforeClass - public void login() { - new LoginPageHelper(driver).authorizationTenant(); - sideBarMenuView = new SideBarMenuViewElements(driver); - ruleChainsPage = new RuleChainsPageHelper(driver); - } - - @AfterMethod - public void delete() { - if (ruleChainName != null) { - testRestClient.deleteRuleChain(getRuleChainByName(ruleChainName).getId()); - ruleChainName = null; - } - } - - @Epic("Rule chains smoke tests") - @Feature("Edit rule chain") @Test(priority = 10, groups = "smoke") @Description("Change name by edit menu") public void changeName() { String newRuleChainName = "Changed" + getRandomNumber(); - String ruleChainName = ENTITY_NAME + random(); + ruleChainName = ENTITY_NAME + random(); testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName)); - this.ruleChainName = ruleChainName; sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.detailsBtn(ruleChainName).click(); @@ -73,39 +45,33 @@ public class RuleChainEditMenuTest extends AbstractDriverBaseTest { ruleChainsPage.editPencilBtn().click(); ruleChainsPage.changeNameEditMenu(newRuleChainName); ruleChainsPage.doneBtnEditView().click(); - this.ruleChainName = newRuleChainName; + ruleChainName = newRuleChainName; ruleChainsPage.setHeaderName(); String nameAfter = ruleChainsPage.getHeaderName(); - Assert.assertNotEquals(nameBefore, nameAfter); - Assert.assertEquals(newRuleChainName, nameAfter); + assertThat(nameAfter).as("The name has changed").isNotEqualTo(nameBefore); + assertThat(nameAfter).as("The name has changed correctly").isEqualTo(newRuleChainName); } - @Epic("Rule chains smoke tests") - @Feature("Edit rule chain") @Test(priority = 20, groups = "smoke") @Description("Delete name and save") public void deleteName() { - String ruleChainName = ENTITY_NAME + random(); + ruleChainName = ENTITY_NAME + random(); testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName)); - this.ruleChainName = ruleChainName; sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.detailsBtn(ruleChainName).click(); ruleChainsPage.editPencilBtn().click(); ruleChainsPage.changeNameEditMenu(""); - Assert.assertFalse(ruleChainsPage.doneBtnEditViewVisible().isEnabled()); + assertIsDisable(ruleChainsPage.doneBtnEditViewVisible()); } - @Epic("Rule chains smoke tests") - @Feature("Edit rule chain") @Test(priority = 20, groups = "smoke") @Description("Save only with space") public void saveOnlyWithSpace() { - String ruleChainName = ENTITY_NAME +random(); + ruleChainName = ENTITY_NAME + random(); testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName)); - this.ruleChainName = ruleChainName; sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.detailsBtn(ruleChainName).click(); @@ -113,50 +79,44 @@ public class RuleChainEditMenuTest extends AbstractDriverBaseTest { ruleChainsPage.changeNameEditMenu(" "); ruleChainsPage.doneBtnEditView().click(); - Assert.assertNotNull(ruleChainsPage.warningMessage()); - Assert.assertTrue(ruleChainsPage.warningMessage().isDisplayed()); - Assert.assertEquals(ruleChainsPage.warningMessage().getText(), EMPTY_RULE_CHAIN_MESSAGE); + assertIsDisplayed(ruleChainsPage.warningMessage()); + assertThat(ruleChainsPage.warningMessage().getText()).as("Text of warning message").isEqualTo(EMPTY_RULE_CHAIN_MESSAGE); } - @Epic("Rule chains smoke tests") - @Feature("Edit rule chain") @Test(priority = 20, groups = "smoke", dataProviderClass = DataProviderCredential.class, dataProvider = "editMenuDescription") @Description("Write the description and save the changes/Change the description and save the changes/Delete the description and save the changes") public void editDescription(String description, String newDescription, String finalDescription) { - String name = ENTITY_NAME + random(); - testRestClient.postRuleChain(EntityPrototypes.defaultRuleChainPrototype(name, description)); - ruleChainName = name; + ruleChainName = ENTITY_NAME + random(); + testRestClient.postRuleChain(EntityPrototypes.defaultRuleChainPrototype(ruleChainName, description)); sideBarMenuView.ruleChainsBtn().click(); - ruleChainsPage.detailsBtn(name).click(); + ruleChainsPage.detailsBtn(ruleChainName).click(); ruleChainsPage.editPencilBtn().click(); ruleChainsPage.descriptionEntityView().sendKeys(newDescription); ruleChainsPage.doneBtnEditView().click(); ruleChainsPage.setDescription(); - Assert.assertEquals(ruleChainsPage.getDescription(), finalDescription); + assertThat(ruleChainsPage.getDescription()).as("The description changed correctly").isEqualTo(finalDescription); } - @Epic("Rule chains smoke tests") - @Feature("Edit rule chain") - @Test(priority = 20, groups = "smoke") + @Test(priority = 20, groups = "smoke", dataProviderClass = DataProviderCredential.class, dataProvider = "debugMode") @Description("Enable debug mode/Disable debug mode") - public void debugMode() { - String ruleChainName = ENTITY_NAME + random(); - testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName)); - this.ruleChainName = ruleChainName; + public void debugMode(boolean debugMode) { + ruleChainName = ENTITY_NAME + random(); + testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName, debugMode)); sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.detailsBtn(ruleChainName).click(); ruleChainsPage.editPencilBtn().click(); ruleChainsPage.debugCheckboxEdit().click(); ruleChainsPage.doneBtnEditView().click(); - boolean debugMode = ruleChainsPage.debugCheckboxView().getAttribute("class").contains("selected"); - ruleChainsPage.editPencilBtn().click(); - ruleChainsPage.debugCheckboxEdit().click(); - ruleChainsPage.doneBtnEditView().click(); - Assert.assertFalse(ruleChainsPage.debugCheckboxView().getAttribute("class").contains("selected"), "Debug mode disable"); - Assert.assertTrue(debugMode, "Debug mode enable"); + if (debugMode) { + assertThat(ruleChainsPage.debugCheckboxView().getAttribute("class").contains("selected")) + .as("Debug mode is enable").isFalse(); + } else { + assertThat(ruleChainsPage.debugCheckboxView().getAttribute("class").contains("selected")) + .as("Debug mode is enable").isTrue(); + } } -} \ No newline at end of file +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SearchRuleChainTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/SearchRuleChainTest.java similarity index 54% rename from msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SearchRuleChainTest.java rename to msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/SearchRuleChainTest.java index b34f59d420..3360a645ed 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SearchRuleChainTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/SearchRuleChainTest.java @@ -13,60 +13,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; +package org.thingsboard.server.msa.ui.tests.rulechainssmoke; import io.qameta.allure.Description; -import io.qameta.allure.Epic; import io.qameta.allure.Feature; -import org.testng.Assert; -import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; -import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; -import org.thingsboard.server.msa.ui.pages.LoginPageHelper; -import org.thingsboard.server.msa.ui.pages.RuleChainsPageHelper; -import org.thingsboard.server.msa.ui.pages.SideBarMenuViewElements; import org.thingsboard.server.msa.ui.utils.DataProviderCredential; +import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultRuleChainPrototype; -public class SearchRuleChainTest extends AbstractDriverBaseTest { +@Feature("Search rule chain") +public class SearchRuleChainTest extends AbstractRuleChainTest { - private SideBarMenuViewElements sideBarMenuView; - private RuleChainsPageHelper ruleChainsPage; - - @BeforeClass - public void login() { - new LoginPageHelper(driver).authorizationTenant(); - sideBarMenuView = new SideBarMenuViewElements(driver); - ruleChainsPage = new RuleChainsPageHelper(driver); - } - - @Epic("Rule chains smoke tests") - @Feature("Search rule chain") @Test(priority = 10, groups = "smoke", dataProviderClass = DataProviderCredential.class, dataProvider = "ruleChainNameForSearchByFirstAndSecondWord") @Description("Search rule chain by first word in the name/Search rule chain by second word in the name") public void searchFirstWord(String namePath) { sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.searchEntity(namePath); - ruleChainsPage.setRuleChainName(0); - Assert.assertTrue(ruleChainsPage.getRuleChainName().contains(namePath)); + ruleChainsPage.allNames().forEach(rc -> assertThat(rc.getText().contains(namePath)) + .as("All entity contains search input").isTrue()); } - @Epic("Rule chains smoke tests") - @Feature("Search rule chain") @Test(priority = 10, groups = "smoke", dataProviderClass = DataProviderCredential.class, dataProvider = "nameForSearchBySymbolAndNumber") @Description("Search rule chain by symbol in the name/Search rule chain by number in the name") public void searchNumber(String name, String namePath) { - testRestClient.postRuleChain(defaultRuleChainPrototype(name)); + ruleChainName = name; + testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName)); sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.searchEntity(namePath); - ruleChainsPage.setRuleChainName(0); - boolean ruleChainContainsNamePath = ruleChainsPage.getRuleChainName().contains(namePath); - - testRestClient.deleteRuleChain(getRuleChainByName(name).getId()); - Assert.assertTrue(ruleChainContainsNamePath); + ruleChainsPage.allNames().forEach(rc -> assertThat(rc.getText().contains(namePath)) + .as("All entity contains search input").isTrue()); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SortByNameTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/SortByNameTest.java similarity index 63% rename from msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SortByNameTest.java rename to msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/SortByNameTest.java index 3a5f02d61d..03b637ed8e 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SortByNameTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/SortByNameTest.java @@ -13,57 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; +package org.thingsboard.server.msa.ui.tests.rulechainssmoke; import io.qameta.allure.Description; import io.qameta.allure.Epic; import io.qameta.allure.Feature; -import org.testng.Assert; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; -import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; -import org.thingsboard.server.msa.ui.pages.LoginPageHelper; -import org.thingsboard.server.msa.ui.pages.RuleChainsPageHelper; -import org.thingsboard.server.msa.ui.pages.SideBarMenuViewElements; import org.thingsboard.server.msa.ui.utils.DataProviderCredential; +import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultRuleChainPrototype; -public class SortByNameTest extends AbstractDriverBaseTest { +@Feature("Sort rule chain by name") +public class SortByNameTest extends AbstractRuleChainTest { - private SideBarMenuViewElements sideBarMenuView; - private RuleChainsPageHelper ruleChainsPage; - private String ruleChainName; - - @BeforeClass - public void login() { - new LoginPageHelper(driver).authorizationTenant(); - sideBarMenuView = new SideBarMenuViewElements(driver); - ruleChainsPage = new RuleChainsPageHelper(driver); - } - - @AfterMethod - public void delete() { - if (ruleChainName != null) { - testRestClient.deleteRuleChain(getRuleChainByName(ruleChainName).getId()); - ruleChainName = null; - } - } - - @Epic("Rule chains smoke tests") - @Feature("Sort rule chain by name") @Test(priority = 10, groups = "smoke", dataProviderClass = DataProviderCredential.class, dataProvider = "nameForSort") @Description("Sort rule chain 'UP'") - public void specialCharacterUp(String ruleChainName) { + public void specialCharacterUp(String name) { + ruleChainName = name; testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName)); - this.ruleChainName = ruleChainName; sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.sortByNameBtn().click(); ruleChainsPage.setRuleChainName(0); - Assert.assertEquals(ruleChainsPage.getRuleChainName(), ruleChainName); + assertThat(ruleChainsPage.getRuleChainName()).as("First rule chain after sort").isEqualTo(ruleChainName); } @Epic("Rule chains smoke tests") @@ -84,13 +58,13 @@ public class SortByNameTest extends AbstractDriverBaseTest { ruleChainsPage.setRuleChainName(2); String thirdRuleChain = ruleChainsPage.getRuleChainName(); - testRestClient.deleteRuleChain(getRuleChainByName(ruleChain).getId()); - testRestClient.deleteRuleChain(getRuleChainByName(ruleChainNumber).getId()); - testRestClient.deleteRuleChain(getRuleChainByName(ruleChainSymbol).getId()); + deleteRuleChainByName(ruleChain); + deleteRuleChainByName(ruleChainNumber); + deleteRuleChainByName(ruleChainSymbol); - Assert.assertEquals(firstRuleChain, ruleChainSymbol); - Assert.assertEquals(secondRuleChain, ruleChainNumber); - Assert.assertEquals(thirdRuleChain, ruleChain); + assertThat(firstRuleChain).as("First rule chain with symbol in name").isEqualTo(ruleChainSymbol); + assertThat(secondRuleChain).as("Second rule chain with number in name").isEqualTo(ruleChainNumber); + assertThat(thirdRuleChain).as("Third rule chain with number in name").isEqualTo(ruleChain); } @Epic("Rule chains smoke tests") @@ -105,7 +79,7 @@ public class SortByNameTest extends AbstractDriverBaseTest { ruleChainsPage.sortByNameDown(); ruleChainsPage.setRuleChainName(ruleChainsPage.allNames().size() - 1); - Assert.assertEquals(ruleChainsPage.getRuleChainName(), ruleChainName); + assertThat(ruleChainsPage.getRuleChainName()).as("Last rule chain after sort").isEqualTo(ruleChainName); } @Epic("Rule chains smoke tests") @@ -127,12 +101,12 @@ public class SortByNameTest extends AbstractDriverBaseTest { ruleChainsPage.setRuleChainName(lastIndex - 2); String thirdRuleChain = ruleChainsPage.getRuleChainName(); - testRestClient.deleteRuleChain(getRuleChainByName(ruleChain).getId()); - testRestClient.deleteRuleChain(getRuleChainByName(ruleChainNumber).getId()); - testRestClient.deleteRuleChain(getRuleChainByName(ruleChainSymbol).getId()); + deleteRuleChainByName(ruleChain); + deleteRuleChainByName(ruleChainNumber); + deleteRuleChainByName(ruleChainSymbol); - Assert.assertEquals(firstRuleChain, ruleChainSymbol); - Assert.assertEquals(secondRuleChain, ruleChainNumber); - Assert.assertEquals(thirdRuleChain, ruleChain); + assertThat(firstRuleChain).as("First from the end rule chain with symbol in name").isEqualTo(ruleChainSymbol); + assertThat(secondRuleChain).as("Second from the end rule chain with number in name").isEqualTo(ruleChainNumber); + assertThat(thirdRuleChain).as("Third rule from the end chain with number in name").isEqualTo(ruleChain); } -} \ No newline at end of file +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SortByTimeTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/SortByTimeTest.java similarity index 52% rename from msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SortByTimeTest.java rename to msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/SortByTimeTest.java index 6c8a912526..95265f8ee6 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SortByTimeTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/rulechainssmoke/SortByTimeTest.java @@ -13,70 +13,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; +package org.thingsboard.server.msa.ui.tests.rulechainssmoke; import io.qameta.allure.Description; -import io.qameta.allure.Epic; import io.qameta.allure.Feature; -import org.testng.Assert; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; -import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; -import org.thingsboard.server.msa.ui.pages.LoginPageHelper; -import org.thingsboard.server.msa.ui.pages.RuleChainsPageHelper; -import org.thingsboard.server.msa.ui.pages.SideBarMenuViewElements; +import static org.assertj.core.api.Assertions.assertThat; import static org.thingsboard.server.msa.ui.utils.Const.ENTITY_NAME; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultRuleChainPrototype; -public class SortByTimeTest extends AbstractDriverBaseTest { +@Feature("Sort rule chain by time") +public class SortByTimeTest extends AbstractRuleChainTest { - private SideBarMenuViewElements sideBarMenuView; - private RuleChainsPageHelper ruleChainsPage; - private String ruleChainName; - - @BeforeClass - public void login() { - new LoginPageHelper(driver).authorizationTenant(); - sideBarMenuView = new SideBarMenuViewElements(driver); - ruleChainsPage = new RuleChainsPageHelper(driver); - } - - @AfterMethod - public void delete() { - if (ruleChainName != null) { - testRestClient.deleteRuleChain(getRuleChainByName(ruleChainName).getId()); - ruleChainName = null; - } - } - - @Epic("Rule chains smoke tests") - @Feature("Sort rule chain by time") @Test(priority = 10, groups = "smoke") @Description("Sort rule chain 'DOWN'") public void sortByTimeDown() { - String ruleChain = ENTITY_NAME; - testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChain)); - ruleChainName = ruleChain; + ruleChainName = ENTITY_NAME; + testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName)); sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.setSort(); String firstListElement = ruleChainsPage.getSort().get(ruleChainsPage.getSort().size() - 1); String lastCreated = ruleChainsPage.createdTime().get(0).getText(); - Assert.assertEquals(firstListElement, lastCreated); - Assert.assertNotNull(ruleChainsPage.createdTimeEntity(ruleChain, lastCreated)); + assertThat(firstListElement).as("Last in list is last created").isEqualTo(lastCreated); + assertIsDisplayed(ruleChainsPage.createdTimeEntity(ruleChainName, lastCreated)); } - @Epic("Rule chains smoke tests") - @Feature("Sort rule chain by time") @Test(priority = 10, groups = "smoke") @Description("Sort rule chain 'UP'") public void sortByTimeUp() { - String ruleChain = ENTITY_NAME; - testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChain)); - ruleChainName = ruleChain; + ruleChainName = ENTITY_NAME; + testRestClient.postRuleChain(defaultRuleChainPrototype(ruleChainName)); sideBarMenuView.ruleChainsBtn().click(); ruleChainsPage.sortByTimeBtn().click(); @@ -84,7 +53,7 @@ public class SortByTimeTest extends AbstractDriverBaseTest { String firstListElement = ruleChainsPage.getSort().get(ruleChainsPage.getSort().size() - 1); String lastCreated = ruleChainsPage.createdTime().get(ruleChainsPage.createdTime().size() - 1).getText(); - Assert.assertEquals(firstListElement, lastCreated); - Assert.assertNotNull(ruleChainsPage.createdTimeEntity(ruleChain, lastCreated)); + assertThat(firstListElement).as("First in list is last created").isEqualTo(lastCreated); + assertIsDisplayed(ruleChainsPage.createdTimeEntity(ruleChainName, lastCreated)); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/DataProviderCredential.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/DataProviderCredential.java index 73783544a1..6e56abff2e 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/DataProviderCredential.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/DataProviderCredential.java @@ -149,4 +149,11 @@ public class DataProviderCredential { {description, newDescription, description + newDescription}, {description, Keys.CONTROL + "A" + Keys.BACK_SPACE, ""}}; } + + @DataProvider + public static Object[][] debugMode() { + return new Object[][]{ + {false}, + {true}}; + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java index 15452506ed..0312817163 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/utils/EntityPrototypes.java @@ -57,6 +57,13 @@ public class EntityPrototypes { return customer; } + public static Customer defaultCustomerPrototype(String entityName, int number) { + Customer customer = new Customer(); + customer.setTitle(entityName); + customer.setPhone("+1" + number); + return customer; + } + public static RuleChain defaultRuleChainPrototype(String entityName) { RuleChain ruleChain = new RuleChain(); ruleChain.setName(entityName); @@ -70,6 +77,13 @@ public class EntityPrototypes { return ruleChain; } + public static RuleChain defaultRuleChainPrototype(String entityName, boolean debugMode) { + RuleChain ruleChain = new RuleChain(); + ruleChain.setName(entityName); + ruleChain.setDebugMode(debugMode); + return ruleChain; + } + public static DeviceProfile defaultDeviceProfile(String entityName) { DeviceProfile deviceProfile = new DeviceProfile(); deviceProfile.setName(entityName); diff --git a/msa/black-box-tests/src/test/resources/all.xml b/msa/black-box-tests/src/test/resources/all.xml index 1fdc4306a9..155aac38e1 100644 --- a/msa/black-box-tests/src/test/resources/all.xml +++ b/msa/black-box-tests/src/test/resources/all.xml @@ -34,7 +34,7 @@ - + diff --git a/msa/black-box-tests/src/test/resources/smokesRuleChain.xml b/msa/black-box-tests/src/test/resources/smokesRuleChain.xml index dbb22782ca..687c6329ba 100644 --- a/msa/black-box-tests/src/test/resources/smokesRuleChain.xml +++ b/msa/black-box-tests/src/test/resources/smokesRuleChain.xml @@ -29,7 +29,7 @@ - + \ No newline at end of file diff --git a/msa/black-box-tests/src/test/resources/uiTests.xml b/msa/black-box-tests/src/test/resources/uiTests.xml index f460c4398d..9dbf8dfc0c 100644 --- a/msa/black-box-tests/src/test/resources/uiTests.xml +++ b/msa/black-box-tests/src/test/resources/uiTests.xml @@ -29,7 +29,7 @@ - + diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml index 2f73ab31f7..352f94e091 100644 --- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml +++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml @@ -70,6 +70,8 @@ queue: max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}" max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}" fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" + request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms + session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" confluent: ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" @@ -88,11 +90,12 @@ queue: # tb_rule_engine.sq: # - key: max.poll.records # value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" - other: # In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside - - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms - value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) - - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms - value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) + other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000" + other: # DEPRECATED. In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside + # - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms + # value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) + # - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms + # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java index 5fbd65e711..3f7c22576f 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java +++ b/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; 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.rule.RuleChainType; @@ -38,6 +39,8 @@ public @interface RuleNode { Class configClazz(); + ComponentClusteringMode clusteringMode() default ComponentClusteringMode.ENABLED; + boolean inEnabled() default true; boolean outEnabled() default true; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java index 22b7e2115b..d2692993de 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java @@ -25,6 +25,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.util.EntityContainer; +import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; @@ -171,13 +172,12 @@ public class TbCreateRelationNode extends TbAbstractRelationActionNode processDevice(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) { - return Futures.transformAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), new DeviceId(entityContainer.getEntityId().getId())), device -> { - if (device != null) { - return processSave(ctx, sdId, relationType); - } else { - return Futures.immediateFuture(true); - } - }, ctx.getDbCallbackExecutor()); + Device device = ctx.getDeviceService().findDeviceById(ctx.getTenantId(), new DeviceId(entityContainer.getEntityId().getId())); + if (device != null) { + return processSave(ctx, sdId, relationType); + } else { + return Futures.immediateFuture(true); + } } private ListenableFuture processAsset(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java index 6c3d80dade..ff6a9ffd77 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java @@ -92,12 +92,14 @@ public class TbLogNode implements TbNode { boolean isStandard(TbLogNodeConfiguration conf) { Objects.requireNonNull(conf, "node config is null"); final TbLogNodeConfiguration defaultConfig = new TbLogNodeConfiguration().defaultConfiguration(); - switch (conf.getScriptLang()) { - case JS: return defaultConfig.getJsScript().equals(conf.getJsScript()); - case TBEL: return defaultConfig.getTbelScript().equals(conf.getTbelScript()); - default: - log.warn("No rule to define isStandard script for script language [{}], assuming that is non-standard", conf.getScriptLang()); - return false; + + if (conf.getScriptLang() == null || conf.getScriptLang().equals(ScriptLanguage.JS)) { + return defaultConfig.getJsScript().equals(conf.getJsScript()); + } else if (conf.getScriptLang().equals(ScriptLanguage.TBEL)) { + return defaultConfig.getTbelScript().equals(conf.getTbelScript()); + } else { + log.warn("No rule to define isStandard script for script language [{}], assuming that is non-standard", conf.getScriptLang()); + return false; } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java index beae54dce6..c408e45d2d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java @@ -46,11 +46,13 @@ import java.util.concurrent.TimeUnit; type = ComponentType.TRANSFORMATION, name = "deduplication", configClazz = TbMsgDeduplicationNodeConfiguration.class, - nodeDescription = "Deduplicate messages for a configurable period based on a specified deduplication strategy.", + nodeDescription = "Deduplicate messages within the same originator entity for a configurable period " + + "based on a specified deduplication strategy.", nodeDetails = "Rule node allows you to select one of the following strategy to deduplicate messages:

" + "FIRST - return first message that arrived during deduplication period.

" + "LAST - return last message that arrived during deduplication period.

" + - "ALL - return all messages as a single JSON array message. Where each element represents object with msg and metadata inner properties.

", + "ALL - return all messages as a single JSON array message. " + + "Where each element represents object with msg and metadata inner properties.

", icon = "content_copy", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeMsgDeduplicationConfig" diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java index dd522ecc8e..16649f8f85 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.msg.session.SessionMsgType; configClazz = EmptyNodeConfiguration.class, relationTypes = {"Post attributes", "Post telemetry", "RPC Request from Device", "RPC Request to Device", "RPC Queued", "RPC Sent", "RPC Delivered", "RPC Successful", "RPC Timeout", "RPC Expired", "RPC Failed", "RPC Deleted", "Activity Event", "Inactivity Event", "Connect Event", "Disconnect Event", "Entity Created", "Entity Updated", "Entity Deleted", "Entity Assigned", - "Entity Unassigned", "Attributes Updated", "Attributes Deleted", "Alarm Acknowledged", "Alarm Cleared", "Other", "Entity Assigned From Tenant", "Entity Assigned To Tenant", + "Entity Unassigned", "Attributes Updated", "Attributes Deleted", "Alarm Acknowledged", "Alarm Cleared", "Alarm Assigned", "Alarm Unassigned", "Comment Created", "Comment Updated", "Other", "Entity Assigned From Tenant", "Entity Assigned To Tenant", "Relation Added or Updated", "Relation Deleted", "All Relations Deleted", "Timeseries Updated", "Timeseries Deleted"}, nodeDescription = "Route incoming messages by Message Type", nodeDetails = "Sends messages with message types \"Post attributes\", \"Post telemetry\", \"RPC Request\" etc. via corresponding chain, otherwise Other chain is used.", @@ -85,6 +85,14 @@ public class TbMsgTypeSwitchNode implements TbNode { relationType = "Alarm Acknowledged"; } else if (msg.getType().equals(DataConstants.ALARM_CLEAR)) { relationType = "Alarm Cleared"; + } else if (msg.getType().equals(DataConstants.ALARM_ASSIGNED)) { + relationType = "Alarm Assigned"; + } else if (msg.getType().equals(DataConstants.ALARM_UNASSIGNED)) { + relationType = "Alarm Unassigned"; + } else if (msg.getType().equals(DataConstants.COMMENT_CREATED)) { + relationType = "Comment Created"; + } else if (msg.getType().equals(DataConstants.COMMENT_UPDATED)) { + relationType = "Comment Updated"; } else if (msg.getType().equals(DataConstants.RPC_CALL_FROM_SERVER_TO_DEVICE)) { relationType = "RPC Request to Device"; } else if (msg.getType().equals(DataConstants.ENTITY_ASSIGNED_FROM_TENANT)) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java index 928b49318e..e7b0a80930 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java @@ -39,7 +39,6 @@ import java.lang.reflect.Type; import java.util.Map; import static org.thingsboard.common.util.DonAsynchron.withCallback; -import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; @Slf4j public abstract class TbAbstractGetEntityDetailsNode implements TbNode { @@ -67,7 +66,7 @@ public abstract class TbAbstractGetEntityDetailsNode getDetails(TbContext ctx, TbMsg msg); - protected abstract ListenableFuture getContactBasedListenableFuture(TbContext ctx, TbMsg msg); + protected abstract ListenableFuture getContactBasedListenableFuture(TbContext ctx, TbMsg msg); protected MessageData getDataAsJson(TbMsg msg) { if (this.config.isAddToMetadata()) { @@ -79,7 +78,7 @@ public abstract class TbAbstractGetEntityDetailsNode getTbMsgListenableFuture(TbContext ctx, TbMsg msg, MessageData messageData, String prefix) { if (!this.config.getDetailsList().isEmpty()) { - ListenableFuture contactBasedListenableFuture = getContactBasedListenableFuture(ctx, msg); + ListenableFuture contactBasedListenableFuture = getContactBasedListenableFuture(ctx, msg); ListenableFuture resultObject = addContactProperties(messageData.getData(), contactBasedListenableFuture, prefix); return transformMsg(ctx, msg, resultObject, messageData); } else { @@ -102,7 +101,7 @@ public abstract class TbAbstractGetEntityDetailsNode addContactProperties(JsonElement data, ListenableFuture entityFuture, String prefix) { + private ListenableFuture addContactProperties(JsonElement data, ListenableFuture entityFuture, String prefix) { return Futures.transformAsync(entityFuture, contactBased -> { if (contactBased != null) { JsonElement jsonElement = null; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java index fa4f3552bc..bf9c689b30 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java @@ -26,6 +26,8 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.ContactBased; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.HasCustomerId; +import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; @@ -59,81 +61,44 @@ public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode getContactBasedListenableFuture(TbContext ctx, TbMsg msg) { - return Futures.transformAsync(getCustomer(ctx, msg), customer -> { - if (customer != null) { - return Futures.immediateFuture(customer); - } else { - return Futures.immediateFuture(null); - } - }, MoreExecutors.directExecutor()); + protected ListenableFuture getContactBasedListenableFuture(TbContext ctx, TbMsg msg) { + return getCustomer(ctx, msg); } private ListenableFuture getCustomer(TbContext ctx, TbMsg msg) { - switch (msg.getOriginator().getEntityType()) { + ListenableFuture entityFuture; + switch (msg.getOriginator().getEntityType()) { // TODO: use EntityServiceRegistry case DEVICE: - return Futures.transformAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), new DeviceId(msg.getOriginator().getId())), device -> { - if (device != null) { - if (!device.getCustomerId().isNullUid()) { - return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), device.getCustomerId()); - } else { - throw new RuntimeException("Device with name '" + device.getName() + "' is not assigned to Customer."); - } - } else { - return Futures.immediateFuture(null); - } - }, MoreExecutors.directExecutor()); + entityFuture = Futures.immediateFuture(ctx.getDeviceService().findDeviceById(ctx.getTenantId(), (DeviceId) msg.getOriginator())); + break; case ASSET: - return Futures.transformAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), new AssetId(msg.getOriginator().getId())), asset -> { - if (asset != null) { - if (!asset.getCustomerId().isNullUid()) { - return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), asset.getCustomerId()); - } else { - throw new RuntimeException("Asset with name '" + asset.getName() + "' is not assigned to Customer."); - } - } else { - return Futures.immediateFuture(null); - } - }, MoreExecutors.directExecutor()); + entityFuture = ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) msg.getOriginator()); + break; case ENTITY_VIEW: - return Futures.transformAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), new EntityViewId(msg.getOriginator().getId())), entityView -> { - if (entityView != null) { - if (!entityView.getCustomerId().isNullUid()) { - return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), entityView.getCustomerId()); - } else { - throw new RuntimeException("EntityView with name '" + entityView.getName() + "' is not assigned to Customer."); - } - } else { - return Futures.immediateFuture(null); - } - }, MoreExecutors.directExecutor()); + entityFuture = ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), (EntityViewId) msg.getOriginator()); + break; case USER: - return Futures.transformAsync(ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), new UserId(msg.getOriginator().getId())), user -> { - if (user != null) { - if (!user.getCustomerId().isNullUid()) { - return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), user.getCustomerId()); - } else { - throw new RuntimeException("User with name '" + user.getName() + "' is not assigned to Customer."); - } - } else { - return Futures.immediateFuture(null); - } - }, MoreExecutors.directExecutor()); + entityFuture = ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) msg.getOriginator()); + break; case EDGE: - return Futures.transformAsync(ctx.getEdgeService().findEdgeByIdAsync(ctx.getTenantId(), new EdgeId(msg.getOriginator().getId())), edge -> { - if (edge != null) { - if (!edge.getCustomerId().isNullUid()) { - return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), edge.getCustomerId()); - } else { - throw new RuntimeException("Edge with name '" + edge.getName() + "' is not assigned to Customer."); - } - } else { - return Futures.immediateFuture(null); - } - }, MoreExecutors.directExecutor()); + entityFuture = ctx.getEdgeService().findEdgeByIdAsync(ctx.getTenantId(), (EdgeId) msg.getOriginator()); + break; default: - throw new RuntimeException("Entity with entityType '" + msg.getOriginator().getEntityType() + "' is not supported."); + throw new RuntimeException(msg.getOriginator().getEntityType().getNormalName() + " entities not supported"); } + return Futures.transformAsync(entityFuture, entity -> { + if (entity != null) { + if (!entity.getCustomerId().isNullUid()) { + return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), entity.getCustomerId()); + } else { + throw new RuntimeException(msg.getOriginator().getEntityType().getNormalName() + + (entity instanceof HasName ? " with name '" + ((HasName) entity).getName() + "'" : "") + + " is not assigned to Customer"); + } + } else { + return Futures.immediateFuture(null); + } + }, MoreExecutors.directExecutor()); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java index 2b7316a59b..6a8de60e6e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java @@ -53,13 +53,7 @@ public class TbGetTenantDetailsNode extends TbAbstractGetEntityDetailsNode getContactBasedListenableFuture(TbContext ctx, TbMsg msg) { - return Futures.transformAsync(ctx.getTenantService().findTenantByIdAsync(ctx.getTenantId(), ctx.getTenantId()), tenant -> { - if (tenant != null) { - return Futures.immediateFuture(tenant); - } else { - return Futures.immediateFuture(null); - } - }, MoreExecutors.directExecutor()); + protected ListenableFuture getContactBasedListenableFuture(TbContext ctx, TbMsg msg) { + return ctx.getTenantService().findTenantByIdAsync(ctx.getTenantId(), ctx.getTenantId()); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index d1feae6271..86b03cf62e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/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.ClientCredentials; 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.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -47,6 +48,7 @@ import java.util.concurrent.TimeoutException; type = ComponentType.EXTERNAL, name = "mqtt", configClazz = TbMqttNodeConfiguration.class, + clusteringMode = ComponentClusteringMode.USER_PREFERENCE, nodeDescription = "Publish messages to the MQTT broker", nodeDetails = "Will publish message payload to the MQTT broker with QoS AT_LEAST_ONCE.", uiResources = {"static/rulenode/rulenode-core-config.js"}, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java index 34c26d820d..5720f66038 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java +++ b/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; import io.netty.handler.codec.mqtt.MqttVersion; -import io.netty.handler.ssl.SslContext; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.common.util.AzureIotHubUtil; import org.thingsboard.mqtt.MqttClientConfig; 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.TbNodeException; 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.ClientCredentials; import org.thingsboard.rule.engine.credentials.CredentialsType; import org.thingsboard.rule.engine.mqtt.TbMqttNode; import org.thingsboard.rule.engine.mqtt.TbMqttNodeConfiguration; +import org.thingsboard.server.common.data.plugin.ComponentClusteringMode; import org.thingsboard.server.common.data.plugin.ComponentType; import javax.net.ssl.SSLException; @@ -41,6 +39,7 @@ import javax.net.ssl.SSLException; type = ComponentType.EXTERNAL, name = "azure iot hub", configClazz = TbAzureIotHubNodeConfiguration.class, + clusteringMode = ComponentClusteringMode.SINGLETON, nodeDescription = "Publish messages to the Azure IoT Hub", nodeDetails = "Will publish message payload to the Azure IoT Hub with QoS AT_LEAST_ONCE.", uiResources = {"static/rulenode/rulenode-core-config.js"}, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java index d36162196e..215f044729 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java @@ -31,7 +31,6 @@ public class EntitiesCustomerIdAsyncLoader { public static ListenableFuture findEntityIdAsync(TbContext ctx, EntityId original) { - switch (original.getEntityType()) { case CUSTOMER: return Futures.immediateFuture((CustomerId) original); @@ -40,7 +39,7 @@ public class EntitiesCustomerIdAsyncLoader { case ASSET: return getCustomerAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) original)); case DEVICE: - return getCustomerAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), (DeviceId) original)); + return getCustomerAsync(Futures.immediateFuture(ctx.getDeviceService().findDeviceById(ctx.getTenantId(), (DeviceId) original))); default: return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original.getEntityType())); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoader.java index cb9ee7c8b6..51d83b3bfb 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoader.java @@ -37,7 +37,7 @@ import java.util.function.Function; public class EntitiesFieldsAsyncLoader { public static ListenableFuture findAsync(TbContext ctx, EntityId original) { - switch (original.getEntityType()) { + switch (original.getEntityType()) { // TODO: use EntityServiceRegistry case TENANT: return getAsync(ctx.getTenantService().findTenantByIdAsync(ctx.getTenantId(), (TenantId) original), EntityFieldsData::new); @@ -51,7 +51,7 @@ public class EntitiesFieldsAsyncLoader { return getAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) original), EntityFieldsData::new); case DEVICE: - return getAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), (DeviceId) original), + return getAsync(Futures.immediateFuture(ctx.getDeviceService().findDeviceById(ctx.getTenantId(), (DeviceId) original)), EntityFieldsData::new); case ALARM: return getAsync(ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), (AlarmId) original), diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbLogNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbLogNodeTest.java index 21e99af819..f12288da3a 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbLogNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbLogNodeTest.java @@ -110,6 +110,19 @@ public class TbLogNodeTest { } + @Test + void backwardCompatibility_whenScriptLangIsNull() throws TbNodeException { + TbLogNodeConfiguration config = new TbLogNodeConfiguration().defaultConfiguration(); + TbLogNode node = spy(new TbLogNode()); + TbNodeConfiguration tbNodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); + TbContext ctx = mock(TbContext.class); + node.init(ctx, tbNodeConfiguration); + + assertThat(node.isStandard(config)).as("Script is standard for language JS").isTrue(); + verify(node, never()).createScriptEngine(any(), any()); + verify(ctx, never()).createScriptEngine(any(), anyString()); + } + @Test void givenScriptEngineEnum_whenNewAdded_thenFailed() { assertThat(ScriptLanguage.values().length).as("only two ScriptLanguage supported").isEqualTo(2); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/AbstractAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/AbstractAttributeNodeTest.java index 45e4f1c27f..ceef71b4c5 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/AbstractAttributeNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/AbstractAttributeNodeTest.java @@ -219,7 +219,7 @@ public abstract class AbstractAttributeNodeTest { void mockFindDevice(Device device) { when(ctx.getDeviceService()).thenReturn(deviceService); - when(deviceService.findDeviceByIdAsync(any(), eq(device.getId()))).thenReturn(Futures.immediateFuture(device)); + when(deviceService.findDeviceById(any(), eq(device.getId()))).thenReturn(device); } void mockFindAsset(Asset asset) { diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 1bf71fee13..7ea553fe5c 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -167,17 +167,24 @@ queue: max.in.flight.requests.per.connection: "${TB_KAFKA_MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION:5}" buffer.memory: "${TB_BUFFER_MEMORY:33554432}" replication_factor: "${TB_QUEUE_KAFKA_REPLICATION_FACTOR:1}" + max_poll_interval_ms: "${TB_QUEUE_KAFKA_MAX_POLL_INTERVAL_MS:300000}" + max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}" + max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}" + fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" + request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms + session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" confluent: ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" sasl.mechanism: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_MECHANISM:PLAIN}" sasl.config: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_JAAS_CONFIG:org.apache.kafka.common.security.plain.PlainLoginModule required username=\"CLUSTER_API_KEY\" password=\"CLUSTER_API_SECRET\";}" security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" - other: # In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside - - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms - value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) - - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms - value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) + other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000" + other: # DEPRECATED. In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside + # - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms + # value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) + # - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms + # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index caeb1bfc2a..346ec48eae 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -152,17 +152,24 @@ queue: max.in.flight.requests.per.connection: "${TB_KAFKA_MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION:5}" buffer.memory: "${TB_BUFFER_MEMORY:33554432}" replication_factor: "${TB_QUEUE_KAFKA_REPLICATION_FACTOR:1}" + max_poll_interval_ms: "${TB_QUEUE_KAFKA_MAX_POLL_INTERVAL_MS:300000}" + max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}" + max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}" + fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" + request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms + session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" confluent: ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" sasl.mechanism: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_MECHANISM:PLAIN}" sasl.config: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_JAAS_CONFIG:org.apache.kafka.common.security.plain.PlainLoginModule required username=\"CLUSTER_API_KEY\" password=\"CLUSTER_API_SECRET\";}" security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" - other: # In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside - - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms - value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) - - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms - value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) + other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000" + other: # DEPRECATED. In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside + # - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms + # value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) + # - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms + # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index f930836d16..4e8167d89d 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -233,17 +233,24 @@ queue: max.in.flight.requests.per.connection: "${TB_KAFKA_MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION:5}" buffer.memory: "${TB_BUFFER_MEMORY:33554432}" replication_factor: "${TB_QUEUE_KAFKA_REPLICATION_FACTOR:1}" + max_poll_interval_ms: "${TB_QUEUE_KAFKA_MAX_POLL_INTERVAL_MS:300000}" + max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}" + max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}" + fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" + request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms + session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" confluent: ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" sasl.mechanism: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_MECHANISM:PLAIN}" sasl.config: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_JAAS_CONFIG:org.apache.kafka.common.security.plain.PlainLoginModule required username=\"CLUSTER_API_KEY\" password=\"CLUSTER_API_SECRET\";}" security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" - other: # In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside - - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms - value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) - - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms - value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) + other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000" + other: # DEPRECATED. In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside + # - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms + # value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) + # - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms + # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index ec60357764..1e0b1ebcd4 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -182,17 +182,24 @@ queue: max.in.flight.requests.per.connection: "${TB_KAFKA_MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION:5}" buffer.memory: "${TB_BUFFER_MEMORY:33554432}" replication_factor: "${TB_QUEUE_KAFKA_REPLICATION_FACTOR:1}" + max_poll_interval_ms: "${TB_QUEUE_KAFKA_MAX_POLL_INTERVAL_MS:300000}" + max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}" + max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}" + fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" + request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms + session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" confluent: ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" sasl.mechanism: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_MECHANISM:PLAIN}" sasl.config: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_JAAS_CONFIG:org.apache.kafka.common.security.plain.PlainLoginModule required username=\"CLUSTER_API_KEY\" password=\"CLUSTER_API_SECRET\";}" security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" - other: # In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside - - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms - value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) - - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms - value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) + other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000" + other: # DEPRECATED. In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside + # - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms + # value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) + # - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms + # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 169e464e57..9f086bcbc5 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -128,17 +128,24 @@ queue: max.in.flight.requests.per.connection: "${TB_KAFKA_MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION:5}" buffer.memory: "${TB_BUFFER_MEMORY:33554432}" replication_factor: "${TB_QUEUE_KAFKA_REPLICATION_FACTOR:1}" + max_poll_interval_ms: "${TB_QUEUE_KAFKA_MAX_POLL_INTERVAL_MS:300000}" + max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}" + max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}" + fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" + request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms + session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" confluent: ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" sasl.mechanism: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_MECHANISM:PLAIN}" sasl.config: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_JAAS_CONFIG:org.apache.kafka.common.security.plain.PlainLoginModule required username=\"CLUSTER_API_KEY\" password=\"CLUSTER_API_SECRET\";}" security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" - other: # In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside - - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms - value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) - - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms - value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) + other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000" + other: # DEPRECATED. In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside + # - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms + # value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) + # - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms + # value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) topic-properties: rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" diff --git a/ui-ngx/src/app/core/services/script/node-script-test.service.ts b/ui-ngx/src/app/core/services/script/node-script-test.service.ts index bf119c2d46..1a33275111 100644 --- a/ui-ngx/src/app/core/services/script/node-script-test.service.ts +++ b/ui-ngx/src/app/core/services/script/node-script-test.service.ts @@ -46,10 +46,14 @@ export class NodeScriptTestService { let msgType: string; if (debugIn) { if (debugIn.data) { - msg = JSON.parse(debugIn.data); + try { + msg = JSON.parse(debugIn.data); + } catch (e) {} } if (debugIn.metadata) { - metadata = JSON.parse(debugIn.metadata); + try { + metadata = JSON.parse(debugIn.metadata); + } catch (e) {} } msgType = debugIn.msgType; } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts index 9b67da4b71..e60fe212f3 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts @@ -120,18 +120,11 @@ export class AlarmTableConfig extends EntityTableConfig color: alarmSeverityColors.get(entity.severity) }))); this.columns.push( - new EntityTableColumn('assignee', 'alarm.assignee', '200px', + new EntityTableColumn('assignee', 'alarm.assignee', '240px', (entity) => { return this.getAssigneeTemplate(entity) }, - (entity) => { - return { - display: 'flex', - justifyContent: 'start', - alignItems: 'center', - height: 'inherit' - } - }, + () => ({}), false, () => ({}), (entity) => undefined, @@ -203,8 +196,10 @@ export class AlarmTableConfig extends EntityTableConfig ${this.getUserDisplayName(entity)} ` : - `account_circle - ${this.translate.instant('alarm.unassigned')}` + ` + account_circle + ${this.translate.instant('alarm.unassigned')} + ` } ` } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss index 8de701f3e3..09a2c62c4c 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table.component.scss @@ -21,10 +21,10 @@ } } .assignee-cell { - display: flex; - justify-content: flex-start; - align-items: center; + line-height: normal; .assigned-container { + display: inline-block; + vertical-align: baseline; max-width: 180px; overflow: hidden; white-space: nowrap; @@ -44,14 +44,19 @@ font-weight: 700; } } - .material-icons.unassigned-icon { - width: 28px; - height: 28px; - font-size: 28px; - margin-right: 8px; - color: rgba(0, 0, 0, 0.38); - overflow: visible; + .unassigned-container { + display: inline-flex; + align-items: center; + .material-icons.unassigned-icon { + width: 28px; + height: 28px; + font-size: 28px; + margin-right: 8px; + color: rgba(0, 0, 0, 0.38); + overflow: visible; + } } + } } } diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts index a4b3a1d4de..a6e7e7f89e 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts +++ b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts @@ -467,6 +467,7 @@ export class ImportExportService { const ruleChainNode: RuleNode = { name: '', debugMode: false, + singletonMode: false, type: 'org.thingsboard.rule.engine.flow.TbRuleChainInputNode', configuration: { ruleChainId: ruleChainConnection.targetRuleChainId.id diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html index b2425a0231..ef61c9133f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/home-page/getting-started-widget.component.html @@ -83,11 +83,11 @@
widgets.getting-started.sys-admin.step6.title
- {{ 'admin.settings' | translate }} + {{ 'admin.settings' | translate }}
- + description{{ 'widgets.getting-started.sys-admin.step6.how-to-configure-notifications' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html index b6912f0b00..35a564182c 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html @@ -24,7 +24,7 @@
-
+
rulenode.name @@ -36,9 +36,14 @@ {{ 'rulenode.name-max-length' | translate }} - - {{ 'rulenode.debug-mode' | translate }} - +
+ + {{ 'rulenode.debug-mode' | translate }} + + + {{ 'rulenode.singleton-mode' | translate }} + +