From 2b0103808a898e02a6453d5406d7be16c9ee274a Mon Sep 17 00:00:00 2001 From: Adrien Date: Sat, 20 Jul 2024 09:29:05 +0700 Subject: [PATCH 01/77] Update template.service Implement auto-restart with systemd --- packaging/java/scripts/control/template.service | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packaging/java/scripts/control/template.service b/packaging/java/scripts/control/template.service index 3fee5c88df..235c3a1700 100644 --- a/packaging/java/scripts/control/template.service +++ b/packaging/java/scripts/control/template.service @@ -7,5 +7,9 @@ User=${pkg.user} ExecStart=${pkg.installFolder}/bin/${pkg.name}.jar SuccessExitStatus=143 +# Service shall be restarted in 30 seconds when the service process exits, is killed, or a timeout is reached. +Restart=always # default 'no' +RestartSec=30 + [Install] WantedBy=multi-user.target From d25c43399c25f31e8e16823eaa00c4ec9a16b7a3 Mon Sep 17 00:00:00 2001 From: Carlos Becker Date: Thu, 12 Sep 2024 10:21:41 +0200 Subject: [PATCH 02/77] OAuth2: allow for 'None' auth method to enable pkce code challenge --- .../oauth2/HybridClientRegistrationRepository.java | 12 ++++++++++-- ui-ngx/src/app/shared/models/oauth2.models.ts | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java index c23ab1e3d2..126ee060dd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/HybridClientRegistrationRepository.java @@ -43,6 +43,15 @@ public class HybridClientRegistrationRepository implements ClientRegistrationRep private ClientRegistration toSpringClientRegistration(OAuth2Client oAuth2Client){ String registrationId = oAuth2Client.getUuidId().toString(); + + // NONE is used if we need pkce-based code challenge + ClientAuthenticationMethod authMethod = ClientAuthenticationMethod.NONE; + if (oAuth2Client.getClientAuthenticationMethod().equals("POST")) { + authMethod = ClientAuthenticationMethod.CLIENT_SECRET_POST; + } else if (oAuth2Client.getClientAuthenticationMethod().equals("BASIC")) { + authMethod = ClientAuthenticationMethod.CLIENT_SECRET_BASIC; + } + return ClientRegistration.withRegistrationId(registrationId) .clientName(oAuth2Client.getName()) .clientId(oAuth2Client.getClientId()) @@ -54,8 +63,7 @@ public class HybridClientRegistrationRepository implements ClientRegistrationRep .userInfoUri(oAuth2Client.getUserInfoUri()) .userNameAttributeName(oAuth2Client.getUserNameAttributeName()) .jwkSetUri(oAuth2Client.getJwkSetUri()) - .clientAuthenticationMethod(oAuth2Client.getClientAuthenticationMethod().equals("POST") ? - ClientAuthenticationMethod.CLIENT_SECRET_POST : ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .clientAuthenticationMethod(authMethod) .redirectUri(defaultRedirectUriTemplate) .build(); } diff --git a/ui-ngx/src/app/shared/models/oauth2.models.ts b/ui-ngx/src/app/shared/models/oauth2.models.ts index dd024715d5..bb2de147ad 100644 --- a/ui-ngx/src/app/shared/models/oauth2.models.ts +++ b/ui-ngx/src/app/shared/models/oauth2.models.ts @@ -69,6 +69,7 @@ export interface OAuth2RegistrationInfo { } export enum ClientAuthenticationMethod { + NONE = 'NONE', BASIC = 'BASIC', POST = 'POST' } From 6a726a8c56b6c33f6b5cb77cedfd9a4a6ed394ad Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 17 Sep 2024 14:11:07 +0200 Subject: [PATCH 03/77] cache hashcode for UUIDBased and TbSubscription --- .../server/service/subscription/TbSubscription.java | 8 +++++++- .../server/common/data/id/UUIDBased.java | 13 +++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java index 4324a3514a..23b7591727 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscription.java @@ -27,6 +27,9 @@ import java.util.function.BiConsumer; @AllArgsConstructor public abstract class TbSubscription { + /** Cache the hash code */ + private transient int hash; // Default to 0. The hash code calculated for this object likely never be zero + private final String serviceId; private final String sessionId; private final int subscriptionId; @@ -49,7 +52,10 @@ public abstract class TbSubscription { @Override public int hashCode() { - return Objects.hash(sessionId, subscriptionId, tenantId, entityId, type); + if (hash == 0) { + hash = Objects.hash(sessionId, subscriptionId, tenantId, entityId, type); + } + return hash; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java index e0a1abf9ff..6cf310f7b3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java @@ -25,6 +25,9 @@ public abstract class UUIDBased implements HasUUID, Serializable { private static final long serialVersionUID = 1L; + /** Cache the hash code */ + private transient int hash; // Default to 0. The hash code calculated for this object likely never be zero + private final UUID id; public UUIDBased() { @@ -43,10 +46,12 @@ public abstract class UUIDBased implements HasUUID, Serializable { @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((id == null) ? 0 : id.hashCode()); - return result; + if (hash == 0) { + final int prime = 31; + int result = 1; + hash = prime * result + ((id == null) ? 0 : id.hashCode()); + } + return hash; } @Override From b8c0fae602e04ea0b75255dcfd26251cf2d1a682 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 17 Sep 2024 14:22:06 +0200 Subject: [PATCH 04/77] TenantId.fromUUID() usage instead of new TenantId() refactored. new TenantId() is deprecated. --- .../edge/rpc/processor/tenant/TenantEdgeProcessor.java | 2 +- .../server/service/queue/DefaultTbCoreConsumerService.java | 2 +- .../service/queue/DefaultTbRuleEngineConsumerService.java | 4 ++-- .../java/org/thingsboard/server/common/data/DeviceIdInfo.java | 2 +- .../org/thingsboard/server/common/data/DeviceProfileInfo.java | 2 +- .../server/common/data/asset/AssetProfileInfo.java | 2 +- .../thingsboard/server/common/data/id/EntityIdFactory.java | 2 +- .../java/org/thingsboard/server/common/data/id/TenantId.java | 4 +++- .../transport/lwm2m/server/store/util/LwM2MClientSerDes.java | 2 +- .../server/service/sync/vc/VersionControlRequestCtx.java | 2 +- .../thingsboard/server/dao/model/sql/OAuth2ClientEntity.java | 2 +- .../org/thingsboard/server/dao/model/sql/QueueEntity.java | 2 +- .../thingsboard/server/dao/model/sql/QueueStatsEntity.java | 2 +- .../org/thingsboard/server/dao/tenant/TenantServiceImpl.java | 2 +- .../java/org/thingsboard/rule/engine/util/TenantIdLoader.java | 2 +- 15 files changed, 18 insertions(+), 16 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java index 515dccb987..b5dbeaa7d8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantEdgeProcessor.java @@ -38,7 +38,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class TenantEdgeProcessor extends BaseEdgeProcessor { public DownlinkMsg convertTenantEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { - TenantId tenantId = new TenantId(edgeEvent.getEntityId()); + TenantId tenantId = TenantId.fromUUID(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) { Tenant tenant = tenantService.findTenantById(tenantId); 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 2a64dd3388..3aaa36b068 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 @@ -503,7 +503,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService { if (cacheKeyProto.hasResourceKey()) { return ImageCacheKey.forImage(tenantId, cacheKeyProto.getResourceKey()); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java index c9a6dee588..73810949b3 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java @@ -192,7 +192,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< private void updateQueues(List queueUpdateMsgs) { for (QueueUpdateMsg queueUpdateMsg : queueUpdateMsgs) { log.info("Received queue update msg: [{}]", queueUpdateMsg); - TenantId tenantId = new TenantId(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); + TenantId tenantId = TenantId.fromUUID(new UUID(queueUpdateMsg.getTenantIdMSB(), queueUpdateMsg.getTenantIdLSB())); if (partitionService.isManagedByCurrentService(tenantId)) { QueueId queueId = new QueueId(new UUID(queueUpdateMsg.getQueueIdMSB(), queueUpdateMsg.getQueueIdLSB())); String queueName = queueUpdateMsg.getQueueName(); @@ -212,7 +212,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService< private void deleteQueues(List queueDeleteMsgs) { for (QueueDeleteMsg queueDeleteMsg : queueDeleteMsgs) { log.info("Received queue delete msg: [{}]", queueDeleteMsg); - TenantId tenantId = new TenantId(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); + TenantId tenantId = TenantId.fromUUID(new UUID(queueDeleteMsg.getTenantIdMSB(), queueDeleteMsg.getTenantIdLSB())); QueueKey queueKey = new QueueKey(ServiceType.TB_RULE_ENGINE, queueDeleteMsg.getQueueName(), tenantId); removeConsumer(queueKey).ifPresent(consumer -> consumer.delete(true)); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceIdInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceIdInfo.java index 44f52e6d74..1655033042 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceIdInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceIdInfo.java @@ -35,7 +35,7 @@ public class DeviceIdInfo implements Serializable, HasTenantId { private final DeviceId deviceId; public DeviceIdInfo(UUID tenantId, UUID customerId, UUID deviceId) { - this.tenantId = new TenantId(tenantId); + this.tenantId = TenantId.fromUUID(tenantId); this.customerId = customerId != null ? new CustomerId(customerId) : null; this.deviceId = new DeviceId(deviceId); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java index 3bc879ff76..ceb01da52b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java @@ -63,7 +63,7 @@ public class DeviceProfileInfo extends EntityInfo { public DeviceProfileInfo(UUID uuid, UUID tenantId, String name, String image, UUID defaultDashboardId, DeviceProfileType type, DeviceTransportType transportType) { super(EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE_PROFILE, uuid), name); - this.tenantId = new TenantId(tenantId); + this.tenantId = TenantId.fromUUID(tenantId); this.image = image; this.defaultDashboardId = defaultDashboardId != null ? new DashboardId(defaultDashboardId) : null; this.type = type; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java index 49b3052425..0b5c5d2b7f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetProfileInfo.java @@ -57,7 +57,7 @@ public class AssetProfileInfo extends EntityInfo { public AssetProfileInfo(UUID uuid, UUID tenantId, String name, String image, UUID defaultDashboardId) { super(EntityIdFactory.getByTypeAndUuid(EntityType.ASSET_PROFILE, uuid), name); - this.tenantId = new TenantId(tenantId); + this.tenantId = TenantId.fromUUID(tenantId); this.image = image; this.defaultDashboardId = defaultDashboardId != null ? new DashboardId(defaultDashboardId) : null; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index 5a85e6ce67..d9a747ad54 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -118,7 +118,7 @@ public class EntityIdFactory { public static EntityId getByEdgeEventTypeAndUuid(EdgeEventType edgeEventType, UUID uuid) { switch (edgeEventType) { case TENANT: - return new TenantId(uuid); + return TenantId.fromUUID(uuid); case CUSTOMER: return new CustomerId(uuid); case USER: diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java index e160eb59d0..ff8739c04d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java @@ -40,7 +40,9 @@ public final class TenantId extends UUIDBased implements EntityId { return tenants.computeIfAbsent(id, TenantId::new); } - //default constructor is still available due to possible usage in extensions + // Please, use TenantId.fromUUID instead + // Default constructor is still available due to possible usage in extensions + @Deprecated public TenantId(UUID id) { super(id); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java index 8cafc2bfd0..6eb54e5447 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java @@ -297,7 +297,7 @@ public class LwM2MClientSerDes { if (tenantId != null) { Field tenantIdField = lwM2mClientClass.getDeclaredField("tenantId"); tenantIdField.setAccessible(true); - tenantIdField.set(lwM2mClient, new TenantId(UUID.fromString(tenantId.getAsString()))); + tenantIdField.set(lwM2mClient, TenantId.fromUUID(UUID.fromString(tenantId.getAsString()))); } JsonElement deviceId = o.get("deviceId"); diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlRequestCtx.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlRequestCtx.java index 74c0257c41..affa902bb0 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlRequestCtx.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/VersionControlRequestCtx.java @@ -34,7 +34,7 @@ public class VersionControlRequestCtx { public VersionControlRequestCtx(ToVersionControlServiceMsg msg, RepositorySettings settings) { this.nodeId = msg.getNodeId(); this.requestId = new UUID(msg.getRequestIdMSB(), msg.getRequestIdLSB()); - this.tenantId = new TenantId(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); + this.tenantId = TenantId.fromUUID(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB())); this.settings = settings; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientEntity.java index 8428d346ce..df76374bc1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2ClientEntity.java @@ -168,7 +168,7 @@ public class OAuth2ClientEntity extends BaseSqlEntity { OAuth2Client registration = new OAuth2Client(); registration.setId(new OAuth2ClientId(id)); registration.setCreatedTime(createdTime); - registration.setTenantId(new TenantId(tenantId)); + registration.setTenantId(TenantId.fromUUID(tenantId)); registration.setTitle(title); registration.setAdditionalInfo(additionalInfo); registration.setMapperConfig( diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java index 72716bec12..87849f4a7e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java @@ -97,7 +97,7 @@ public class QueueEntity extends BaseSqlEntity { public Queue toData() { Queue queue = new Queue(new QueueId(getUuid())); queue.setCreatedTime(createdTime); - queue.setTenantId(new TenantId(tenantId)); + queue.setTenantId(TenantId.fromUUID(tenantId)); queue.setName(name); queue.setTopic(topic); queue.setPollInterval(pollInterval); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueStatsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueStatsEntity.java index 96800a4d38..7f24119831 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueStatsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueStatsEntity.java @@ -61,7 +61,7 @@ public class QueueStatsEntity extends BaseSqlEntity { public QueueStats toData() { QueueStats queueStats = new QueueStats(new QueueStatsId(getUuid())); queueStats.setCreatedTime(createdTime); - queueStats.setTenantId(new TenantId(tenantId)); + queueStats.setTenantId(TenantId.fromUUID(tenantId)); queueStats.setQueueName(queueName); queueStats.setServiceId(serviceId); return queueStats; diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 8e6c38814a..1115bfad14 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -235,7 +235,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService> findEntity(TenantId tenantId, EntityId entityId) { - return Optional.ofNullable(findTenantById(new TenantId(entityId.getId()))); + return Optional.ofNullable(findTenantById(TenantId.fromUUID(entityId.getId()))); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java index 1e1b031259..70ddf46ce4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/TenantIdLoader.java @@ -61,7 +61,7 @@ public class TenantIdLoader { HasTenantId tenantEntity; switch (entityType) { case TENANT: - return new TenantId(id); + return TenantId.fromUUID(id); case CUSTOMER: tenantEntity = ctx.getCustomerService().findCustomerById(ctxTenantId, new CustomerId(id)); break; From 19fa00a5fbb87c62eed42e62fd6a463c4678c046 Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Fri, 11 Oct 2024 17:42:20 +0300 Subject: [PATCH 05/77] Rule node debug strategies --- .../rule_chains/edge_root_rule_chain.json | 18 +- .../tenant/rule_chains/root_rule_chain.json | 14 +- .../main/data/upgrade/3.8.0/schema_update.sql | 21 + .../server/actors/ActorSystemContext.java | 4 + .../actors/ruleChain/DefaultTbContext.java | 82 +- .../RuleChainActorMessageProcessor.java | 44 +- .../actors/ruleChain/RuleNodeActor.java | 6 +- .../RuleNodeActorMessageProcessor.java | 37 +- .../server/actors/ruleChain/RuleNodeCtx.java | 17 +- .../RuleNodeToRuleChainTellNextMsg.java | 10 +- .../controller/TenantProfileController.java | 1 + .../BaseRuleChainMetadataConstructor.java | 4 +- .../tenant/TenantMsgConstructorV1.java | 1 + .../src/main/resources/thingsboard.yml | 3 + .../actors/rule/DefaultTbContextTest.java | 836 ++++++++++++++++++ .../server/edge/AbstractEdgeTest.java | 6 +- .../server/edge/RuleChainEdgeTest.java | 3 +- ...AbstractRuleEngineFlowIntegrationTest.java | 11 +- ...actRuleEngineLifecycleIntegrationTest.java | 3 +- .../housekeeper/HousekeeperServiceTest.java | 5 +- .../sync/ie/ExportImportServiceSqlTest.java | 9 +- .../service/sync/vc/VersionControlTest.java | 9 +- .../common/data/msg/TbNodeConnectionType.java | 4 + .../common/data/rule/DebugStrategy.java | 72 ++ .../server/common/data/rule/RuleNode.java | 24 +- .../DefaultTenantProfileConfiguration.java | 10 + .../profile/TenantProfileConfiguration.java | 3 + common/edge-api/src/main/proto/edge.proto | 10 +- .../server/dao/model/ModelConstants.java | 1 + .../server/dao/model/sql/RuleNodeEntity.java | 17 +- .../server/dao/rule/BaseRuleChainService.java | 5 +- .../main/resources/sql/schema-entities.sql | 3 +- 32 files changed, 1165 insertions(+), 128 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/rule/DebugStrategy.java diff --git a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json index 6b7603c026..04a5f52f4e 100644 --- a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json +++ b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json @@ -20,7 +20,7 @@ }, "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", "name": "Device Profile Node", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "persistAlarmRulesState": false, "fetchAlarmRulesStateOnStart": false @@ -34,7 +34,7 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "defaultTTL": 0 }, @@ -47,7 +47,7 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", - "debugMode": false, + "debugStrategy": "DISABLED", "configurationVersion": 2, "configuration": { "scope": "CLIENT_SCOPE", @@ -64,7 +64,7 @@ }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "Message Type Switch", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "version": 0 }, @@ -77,7 +77,7 @@ }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log RPC from Device", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "scriptLang": "TBEL", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", @@ -92,7 +92,7 @@ }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log Other", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "scriptLang": "TBEL", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", @@ -107,7 +107,7 @@ }, "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "name": "RPC Call Request", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "timeoutInSeconds": 60 }, @@ -120,7 +120,7 @@ }, "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "name": "Push to cloud", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "scope": "SERVER_SCOPE" }, @@ -133,7 +133,7 @@ }, "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "name": "Push to cloud", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "scope": "SERVER_SCOPE" }, diff --git a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json index 0b70d087e7..3b9898d0b1 100644 --- a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json +++ b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json @@ -18,7 +18,7 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "defaultTTL": 0 } @@ -30,7 +30,7 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", - "debugMode": false, + "debugStrategy": "DISABLED", "configurationVersion": 2, "configuration": { "scope": "CLIENT_SCOPE", @@ -46,7 +46,7 @@ }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "Message Type Switch", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "version": 0 } @@ -58,7 +58,7 @@ }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log RPC from Device", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "scriptLang": "TBEL", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", @@ -72,7 +72,7 @@ }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log Other", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "scriptLang": "TBEL", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", @@ -86,7 +86,7 @@ }, "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "name": "RPC Call Request", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "timeoutInSeconds": 60 } @@ -99,7 +99,7 @@ }, "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", "name": "Device Profile Node", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "persistAlarmRulesState": false, "fetchAlarmRulesStateOnStart": false diff --git a/application/src/main/data/upgrade/3.8.0/schema_update.sql b/application/src/main/data/upgrade/3.8.0/schema_update.sql index 6b87dc6dde..dc3bf2386e 100644 --- a/application/src/main/data/upgrade/3.8.0/schema_update.sql +++ b/application/src/main/data/upgrade/3.8.0/schema_update.sql @@ -14,3 +14,24 @@ -- limitations under the License. -- +-- UPDATE RULE NODE DEBUG MODE TO DEBUG STRATEGY START + +ALTER TABLE rule_node + ADD COLUMN IF NOT EXISTS debug_strategy varchar(32) DEFAULT 'DISABLED'; +ALTER TABLE rule_node + ADD COLUMN IF NOT EXISTS last_update_ts bigint NOT NULL DEFAULT extract(epoch from now()) * 1000; +DO +$$ + BEGIN + IF EXISTS (SELECT 1 + FROM information_schema.columns + WHERE table_name = 'rule_node' AND column_name = 'debug_mode') THEN + UPDATE rule_node + SET debug_strategy = CASE WHEN debug_mode = true THEN 'ALL_EVENTS' ELSE 'DISABLED' END; + ALTER TABLE rule_node + DROP COLUMN debug_mode; + END IF; + END +$$; + +-- UPDATE RULE NODE DEBUG MODE TO DEBUG STRATEGY END 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 5966716ed8..2920fa400a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -577,6 +577,10 @@ public class ActorSystemContext { @Getter private boolean externalNodeForceAck; + @Value("${actors.rule.node.max_debug_mode_duration:60}") + @Getter + private int maxRuleNodeDebugModeDurationMinutes; + @Value("${state.rule.node.deviceState.rateLimit:1:1,30:60,60:3600}") @Getter private String deviceStateNodeRateLimitConfig; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index ec10402821..3be7524c85 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -63,6 +63,7 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.script.ScriptLanguage; @@ -129,7 +130,7 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.ENTITY_CREATED; * Created by ashvayka on 19.03.18. */ @Slf4j -class DefaultTbContext implements TbContext { +public class DefaultTbContext implements TbContext { private final ActorSystemContext mainCtx; private final String ruleChainName; @@ -143,25 +144,25 @@ class DefaultTbContext implements TbContext { @Override public void tellSuccess(TbMsg msg) { - tellNext(msg, Collections.singleton(TbNodeConnectionType.SUCCESS), null); + tellNext(msg, Collections.singleton(TbNodeConnectionType.SUCCESS)); } @Override public void tellNext(TbMsg msg, String relationType) { - tellNext(msg, Collections.singleton(relationType), null); + tellNext(msg, Collections.singleton(relationType)); } @Override public void tellNext(TbMsg msg, Set relationTypes) { - tellNext(msg, relationTypes, null); - } - - private void tellNext(TbMsg msg, Set relationTypes, Throwable th) { - if (nodeCtx.getSelf().isDebugMode()) { - relationTypes.forEach(relationType -> mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, relationType, th)); + RuleNode ruleNode = nodeCtx.getSelf(); + DebugStrategy debugStrategy = ruleNode.getDebugStrategy(); + if (debugStrategy.shouldPersistDebugOutputForAllEvents(ruleNode.getLastUpdateTs(), msg.getTs(), getMaxRuleNodeDebugDurationMinutes())) { + relationTypes.forEach(relationType -> mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), msg, relationType)); + } else if (debugStrategy.shouldPersistDebugForFailureEventOnly(relationTypes)) { + mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), msg, TbNodeConnectionType.FAILURE); } - msg.getCallback().onProcessingEnd(nodeCtx.getSelf().getId()); - nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId(), relationTypes, msg, th != null ? th.getMessage() : null)); + msg.getCallback().onProcessingEnd(ruleNode.getId()); + nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(ruleNode.getRuleChainId(), ruleNode.getId(), relationTypes, msg, null)); } @Override @@ -182,8 +183,12 @@ class DefaultTbContext implements TbContext { if (item == null) { ack(msg); } else { - if (nodeCtx.getSelf().isDebugMode()) { - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, relationType); + RuleNode ruleNode = nodeCtx.getSelf(); + DebugStrategy debugStrategy = ruleNode.getDebugStrategy(); + if (debugStrategy.shouldPersistDebugOutputForAllEvents(ruleNode.getLastUpdateTs(), msg.getTs(), getMaxRuleNodeDebugDurationMinutes())) { + mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), msg, relationType); + } else if (debugStrategy.shouldPersistDebugForFailureEventOnly(relationType)) { + mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), msg, relationType); } nodeCtx.getChainActor().tell(new RuleChainOutputMsg(item.getRuleChainId(), item.getRuleNodeId(), relationType, msg)); } @@ -213,11 +218,13 @@ class DefaultTbContext implements TbContext { .setTenantIdMSB(getTenantId().getId().getMostSignificantBits()) .setTenantIdLSB(getTenantId().getId().getLeastSignificantBits()) .setTbMsg(TbMsg.toByteString(tbMsg)).build(); - if (nodeCtx.getSelf().isDebugMode()) { - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, "To Root Rule Chain"); - } mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg, new SimpleTbQueueCallback( metadata -> { + RuleNode ruleNode = nodeCtx.getSelf(); + DebugStrategy debugStrategy = ruleNode.getDebugStrategy(); + if (debugStrategy.shouldPersistDebugOutputForAllEvents(ruleNode.getLastUpdateTs(), tbMsg.getTs(), getMaxRuleNodeDebugDurationMinutes())) { + mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), tbMsg, TbNodeConnectionType.TO_ROOT_RULE_CHAIN); + } if (onSuccess != null) { onSuccess.run(); } @@ -299,8 +306,9 @@ class DefaultTbContext implements TbContext { } return; } - RuleChainId ruleChainId = nodeCtx.getSelf().getRuleChainId(); - RuleNodeId ruleNodeId = nodeCtx.getSelf().getId(); + RuleNode ruleNode = nodeCtx.getSelf(); + RuleChainId ruleChainId = ruleNode.getRuleChainId(); + RuleNodeId ruleNodeId = ruleNode.getId(); TbMsg tbMsg = TbMsg.newMsg(source, queueName, ruleChainId, ruleNodeId); TransportProtos.ToRuleEngineMsg.Builder msg = TransportProtos.ToRuleEngineMsg.newBuilder() .setTenantIdMSB(getTenantId().getId().getMostSignificantBits()) @@ -310,12 +318,15 @@ class DefaultTbContext implements TbContext { if (failureMessage != null) { msg.setFailureMessage(failureMessage); } - if (nodeCtx.getSelf().isDebugMode()) { - relationTypes.forEach(relationType -> - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, relationType, null, failureMessage)); - } mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg.build(), new SimpleTbQueueCallback( metadata -> { + DebugStrategy debugStrategy = ruleNode.getDebugStrategy(); + if (debugStrategy.shouldPersistDebugOutputForAllEvents(ruleNode.getLastUpdateTs(), tbMsg.getTs(), getMaxRuleNodeDebugDurationMinutes())) { + relationTypes.forEach(relationType -> + mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), tbMsg, relationType, null, failureMessage)); + } else if (debugStrategy.shouldPersistDebugForFailureEventOnly(relationTypes)) { + mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), tbMsg, TbNodeConnectionType.FAILURE, null, failureMessage); + } if (onSuccess != null) { onSuccess.run(); } @@ -331,10 +342,12 @@ class DefaultTbContext implements TbContext { @Override public void ack(TbMsg tbMsg) { - if (nodeCtx.getSelf().isDebugMode()) { - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, "ACK", null); + RuleNode ruleNode = nodeCtx.getSelf(); + DebugStrategy debugStrategy = ruleNode.getDebugStrategy(); + if (debugStrategy.shouldPersistDebugOutputForAllEvents(ruleNode.getLastUpdateTs(), tbMsg.getTs(), getMaxRuleNodeDebugDurationMinutes())) { + mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), tbMsg, TbNodeConnectionType.ACK); } - tbMsg.getCallback().onProcessingEnd(nodeCtx.getSelf().getId()); + tbMsg.getCallback().onProcessingEnd(ruleNode.getId()); tbMsg.getCallback().onSuccess(); } @@ -349,12 +362,13 @@ class DefaultTbContext implements TbContext { @Override public void tellFailure(TbMsg msg, Throwable th) { - if (nodeCtx.getSelf().isDebugMode()) { - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, TbNodeConnectionType.FAILURE, th); + RuleNode ruleNode = nodeCtx.getSelf(); + if (ruleNode.getDebugStrategy().shouldPersistDebugForFailureEventOnly(ruleNode.getLastUpdateTs(), msg.getTs(), getMaxRuleNodeDebugDurationMinutes())) { + mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), msg, TbNodeConnectionType.FAILURE, th); } String failureMessage = getFailureMessage(th); - nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getRuleChainId(), - nodeCtx.getSelf().getId(), Collections.singleton(TbNodeConnectionType.FAILURE), + nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(ruleNode.getRuleChainId(), + ruleNode.getId(), Collections.singleton(TbNodeConnectionType.FAILURE), msg, failureMessage)); } @@ -1004,4 +1018,14 @@ class DefaultTbContext implements TbContext { return failureMessage; } + private int getMaxRuleNodeDebugDurationMinutes() { + if (!DebugStrategy.ALL_EVENTS.equals(nodeCtx.getSelf().getDebugStrategy())) { + return 0; + } + var configuration = mainCtx.getTenantProfileCache() + .get(getTenantId()).getProfileData().getConfiguration(); + int systemMaxRuleNodeDebugModeDurationMinutes = mainCtx.getMaxRuleNodeDebugModeDurationMinutes(); + return configuration.getMaxRuleNodeDebugModeDurationMinutes(systemMaxRuleNodeDebugModeDurationMinutes); + } + } diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java index eaa2218116..2213daa770 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java @@ -43,7 +43,6 @@ import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg; import org.thingsboard.server.common.msg.queue.RuleEngineException; import org.thingsboard.server.common.msg.queue.RuleNodeException; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.queue.TbQueueCallback; @@ -72,7 +71,6 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor> nodeRoutes; private final RuleChainService service; private final TbClusterService clusterService; - private final TbApiUsageReportClient apiUsageClient; private String ruleChainName; private RuleNodeId firstId; @@ -81,7 +79,6 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor existingNodes = ruleNodeList.stream().map(RuleNode::getId).collect(Collectors.toSet()); - List removedRules = nodeActors.keySet().stream().filter(node -> !existingNodes.contains(node)).collect(Collectors.toList()); + List removedRules = nodeActors.keySet().stream().filter(node -> !existingNodes.contains(node)).toList(); removedRules.forEach(ruleNodeId -> { log.trace("[{}][{}] Removing rule node [{}]", tenantId, entityId, ruleNodeId); RuleNodeCtx removed = nodeActors.remove(ruleNodeId); @@ -177,7 +174,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor relations = service.getRuleNodeRelations(TenantId.SYS_TENANT_ID, ruleNode.getId()); log.trace("[{}][{}][{}] Processing rule node relations [{}]", tenantId, entityId, ruleNode.getId(), relations.size()); - if (relations.size() == 0) { + if (relations.isEmpty()) { nodeRoutes.put(ruleNode.getId(), Collections.emptyList()); } else { for (EntityRelation relation : relations) { @@ -238,45 +235,53 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor relationTypes; - @Getter private final String failureMessage; public RuleNodeToRuleChainTellNextMsg(RuleChainId ruleChainId, RuleNodeId originator, Set relationTypes, TbMsg tbMsg, String failureMessage) { diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index 007f77aa20..85660b5914 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -151,6 +151,7 @@ public class TenantProfileController extends BaseController { " \"maxJSExecutions\": 5000000,\n" + " \"maxDPStorageDays\": 0,\n" + " \"maxRuleNodeExecutionsPerMessage\": 50,\n" + + " \"maxRuleNodeDebugDurationMinutes\": 15,\n" + " \"maxEmails\": 0,\n" + " \"maxSms\": 0,\n" + " \"maxCreatedAlarms\": 1000,\n" + diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/BaseRuleChainMetadataConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/BaseRuleChainMetadataConstructor.java index cb66992565..38b2d959ea 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/BaseRuleChainMetadataConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/rule/BaseRuleChainMetadataConstructor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.gen.edge.v1.DebugStrategy; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.NodeConnectionInfoProto; import org.thingsboard.server.gen.edge.v1.RuleChainConnectionInfoProto; @@ -88,7 +89,8 @@ public abstract class BaseRuleChainMetadataConstructor implements RuleChainMetad .setIdLSB(node.getId().getId().getLeastSignificantBits()) .setType(node.getType()) .setName(node.getName()) - .setDebugMode(node.isDebugMode()) + .setLastUpdateTs(node.getLastUpdateTs()) + .setDebugStrategy(DebugStrategy.forNumber(node.getDebugStrategy().getProtoNumber())) .setConfiguration(JacksonUtil.toString(node.getConfiguration())) .setAdditionalInfo(JacksonUtil.toString(node.getAdditionalInfo())) .setSingletonMode(node.isSingletonMode()) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorV1.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorV1.java index fc244417bc..5d60c69903 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorV1.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/tenant/TenantMsgConstructorV1.java @@ -88,6 +88,7 @@ public class TenantMsgConstructorV1 implements TenantMsgConstructor { configuration.setMaxTransportDataPoints(0); configuration.setRuleEngineExceptionsTtlDays(0); configuration.setMaxRuleNodeExecutionsPerMessage(0); + configuration.setMaxRuleNodeDebugDurationMinutes(0); tenantProfileData.setConfiguration(configuration); tenantProfile.setProfileData(tenantProfileData); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index e9c8668958..581a59fa36 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -468,6 +468,9 @@ actors: node: # Errors for particular actor are persisted once per specified amount of milliseconds error_persist_frequency: "${ACTORS_RULE_NODE_ERROR_FREQUENCY:3000}" + # The maximum allowed duration (in minutes) for the debug mode to be enabled if the debug strategy is set to ALL_EVENTS. + # If a specific value is set in the tenant profile, the minimum between value from profile and this setting will be used. + max_debug_mode_duration: "${ACTORS_RULE_NODE_MAX_DEBUG_MODE_DURATION_MINUTES:60}" transaction: # Size of queues that store messages for transaction rule nodes queue_size: "${ACTORS_RULE_TRANSACTION_QUEUE_SIZE:15000}" diff --git a/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java b/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java new file mode 100644 index 0000000000..e9b344866f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java @@ -0,0 +1,836 @@ +/** + * Copyright © 2016-2024 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.actors.rule; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.actors.TbActorRef; +import org.thingsboard.server.actors.ruleChain.DefaultTbContext; +import org.thingsboard.server.actors.ruleChain.RuleChainOutputMsg; +import org.thingsboard.server.actors.ruleChain.RuleNodeCtx; +import org.thingsboard.server.actors.ruleChain.RuleNodeToRuleChainTellNextMsg; +import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; +import org.thingsboard.server.common.data.rule.DebugStrategy; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.TbMsgProcessingStackItem; +import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TbMsgCallback; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; +import org.thingsboard.server.queue.common.SimpleTbQueueCallback; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + +@SuppressWarnings("ResultOfMethodCallIgnored") +@ExtendWith(MockitoExtension.class) +class DefaultTbContextTest { + + private final String EXCEPTION_MSG = "Some runtime exception!"; + private final RuntimeException EXCEPTION = new RuntimeException(EXCEPTION_MSG); + + private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("c7bf4c85-923c-4688-a4b5-0f8a0feb7cd5")); + private final RuleNodeId RULE_NODE_ID = new RuleNodeId(UUID.fromString("1ca5e2ef-1309-41d9-bafa-709e9df0e2a6")); + private final RuleChainId RULE_CHAIN_ID = new RuleChainId(UUID.fromString("b87c4123-f9f2-41a6-9a09-e3a5b6580b11")); + + @Mock + private ActorSystemContext mainCtxMock; + @Mock + private RuleNodeCtx nodeCtxMock; + @Mock + private TbActorRef chainActorMock; + + private DefaultTbContext defaultTbContext; + + @BeforeEach + public void setUp() { + defaultTbContext = new DefaultTbContext(mainCtxMock, "Test rule chain name", nodeCtxMock); + } + + @Test + public void givenDebugStrategyOnlyFailureEvents_whenTellSuccess_thenVerifyDebugOutputNotPersisted() { + // GIVEN + var callbackMock = mock(TbMsgCallback.class); + var msg = getTbMsgWithCallback(callbackMock); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.ONLY_FAILURE_EVENTS); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); + + // WHEN + defaultTbContext.tellSuccess(msg); + + // THEN + then(nodeCtxMock).should().getChainActor(); + then(nodeCtxMock).shouldHaveNoMoreInteractions(); + then(mainCtxMock).shouldHaveNoInteractions(); + checkTellNextCommonLogic(callbackMock, TbNodeConnectionType.SUCCESS, msg); + } + + @Test + public void givenDebugStrategyOnlyFailureEventsAndSuccessConnection_whenTellNext_thenVerifyDebugOutputNotPersisted() { + // GIVEN + var callbackMock = mock(TbMsgCallback.class); + var msg = getTbMsgWithCallback(callbackMock); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.ONLY_FAILURE_EVENTS); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); + + // WHEN + defaultTbContext.tellNext(msg, TbNodeConnectionType.SUCCESS); + + // THEN + then(nodeCtxMock).should().getChainActor(); + then(nodeCtxMock).shouldHaveNoMoreInteractions(); + then(mainCtxMock).shouldHaveNoInteractions(); + checkTellNextCommonLogic(callbackMock, TbNodeConnectionType.SUCCESS, msg); + } + + @MethodSource + @ParameterizedTest + void givenDebugStrategyOnlyFailureEventsAndConnections_whenTellNext_thenVerifyDebugOutputPersisted(Set connections) { + // GIVEN + var callbackMock = mock(TbMsgCallback.class); + var msg = getTbMsgWithCallback(callbackMock); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.ONLY_FAILURE_EVENTS); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); + + // WHEN + defaultTbContext.tellNext(msg, connections); + + // THEN + then(nodeCtxMock).should().getChainActor(); + then(nodeCtxMock).shouldHaveNoMoreInteractions(); + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, TbNodeConnectionType.FAILURE); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + checkTellNextCommonLogic(callbackMock, connections, msg); + } + + private static Stream> givenDebugStrategyOnlyFailureEventsAndConnections_whenTellNext_thenVerifyDebugOutputPersisted() { + return Stream.of( + Collections.singleton(TbNodeConnectionType.FAILURE), + Set.of(TbNodeConnectionType.FAILURE, TbNodeConnectionType.SUCCESS) + ); + } + + @MethodSource + @ParameterizedTest + void givenDebugStrategyDisabledAndConnections_whenTellNext_thenVerifyDebugOutputNotPersisted(Set connections) { + // GIVEN + var callbackMock = mock(TbMsgCallback.class); + var msg = getTbMsgWithCallback(callbackMock); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.DISABLED); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); + + // WHEN + defaultTbContext.tellNext(msg, connections); + + // THEN + then(nodeCtxMock).should().getChainActor(); + then(nodeCtxMock).shouldHaveNoMoreInteractions(); + then(mainCtxMock).shouldHaveNoInteractions(); + checkTellNextCommonLogic(callbackMock, connections, msg); + } + + private static Stream> givenDebugStrategyDisabledAndConnections_whenTellNext_thenVerifyDebugOutputNotPersisted() { + return Stream.of( + Collections.singleton(TbNodeConnectionType.FAILURE), + Collections.singleton(TbNodeConnectionType.SUCCESS), + Set.of(TbNodeConnectionType.FAILURE, TbNodeConnectionType.SUCCESS) + ); + } + + @MethodSource + @ParameterizedTest + void givenDebugStrategyAllEventsAndConnection_whenTellNext_thenVerifyDebugOutputPersisted(String connection) { + // GIVEN + var callbackMock = mock(TbMsgCallback.class); + var msg = getTbMsgWithCallback(callbackMock); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setLastUpdateTs(System.currentTimeMillis()); + ruleNode.setDebugStrategy(DebugStrategy.ALL_EVENTS); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); + mockGetMaxRuleNodeDebugModeDurationMinutes(); + + // WHEN + defaultTbContext.tellNext(msg, connection); + + // THEN + then(nodeCtxMock).should().getChainActor(); + then(nodeCtxMock).shouldHaveNoMoreInteractions(); + then(mainCtxMock).should().getTenantProfileCache(); + then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, connection); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + checkTellNextCommonLogic(callbackMock, connection, msg); + } + + private static Stream givenDebugStrategyAllEventsAndConnection_whenTellNext_thenVerifyDebugOutputPersisted() { + return failureAndSuccessConnection(); + } + + @Test + public void givenDebugStrategyAllEventsAndFailureAndSuccessConnection_whenTellNext_thenVerifyDebugOutputPersistedForAllEvents() { + // GIVEN + var callbackMock = mock(TbMsgCallback.class); + var msg = getTbMsgWithCallback(callbackMock); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setLastUpdateTs(System.currentTimeMillis()); + ruleNode.setDebugStrategy(DebugStrategy.ALL_EVENTS); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); + mockGetMaxRuleNodeDebugModeDurationMinutes(); + + // WHEN + Set connections = failureAndSuccessConnection().collect(Collectors.toSet()); + defaultTbContext.tellNext(msg, connections); + + // THEN + then(nodeCtxMock).should().getChainActor(); + then(nodeCtxMock).shouldHaveNoMoreInteractions(); + then(mainCtxMock).should().getTenantProfileCache(); + then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); + var nodeConnectionsCaptor = ArgumentCaptor.forClass(String.class); + int wantedNumberOfInvocations = connections.size(); + then(mainCtxMock).should(times(wantedNumberOfInvocations)).persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), eq(msg), nodeConnectionsCaptor.capture()); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + assertThat(nodeConnectionsCaptor.getAllValues()).hasSize(wantedNumberOfInvocations); + assertThat(nodeConnectionsCaptor.getAllValues()).containsExactlyInAnyOrderElementsOf(connections); + checkTellNextCommonLogic(callbackMock, connections, msg); + } + + private static Stream failureAndSuccessConnection() { + return Stream.of(TbNodeConnectionType.FAILURE, TbNodeConnectionType.SUCCESS); + } + + @Test + public void givenDebugStrategyOnlyFailureEventsAndFailureConnection_whenOutput_thenVerifyDebugOutputPersisted() { + // GIVEN + var msgMock = mock(TbMsg.class); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.ONLY_FAILURE_EVENTS); + given(msgMock.popFormStack()).willReturn(new TbMsgProcessingStackItem(RULE_CHAIN_ID, RULE_NODE_ID)); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); + + // WHEN + defaultTbContext.output(msgMock, TbNodeConnectionType.FAILURE); + + // THEN + checkOutputCommonLogic(msgMock, TbNodeConnectionType.FAILURE); + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, TbNodeConnectionType.FAILURE); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + then(nodeCtxMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void givenDebugStrategyOnlyFailureEventsAndSuccessConnection_whenOutput_thenVerifyDebugOutputNotPersisted() { + // GIVEN + var msgMock = mock(TbMsg.class); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.ONLY_FAILURE_EVENTS); + given(msgMock.popFormStack()).willReturn(new TbMsgProcessingStackItem(RULE_CHAIN_ID, RULE_NODE_ID)); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); + + // WHEN + defaultTbContext.output(msgMock, TbNodeConnectionType.SUCCESS); + + // THEN + checkOutputCommonLogic(msgMock, TbNodeConnectionType.SUCCESS); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + then(nodeCtxMock).shouldHaveNoMoreInteractions(); + } + + @ParameterizedTest + @ValueSource(strings = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE}) + void givenDebugStrategyDisabled_whenOutput_thenVerifyDebugOutputNotPersisted(String nodeConnection) { + // GIVEN + var msgMock = mock(TbMsg.class); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.DISABLED); + given(msgMock.popFormStack()).willReturn(new TbMsgProcessingStackItem(RULE_CHAIN_ID, RULE_NODE_ID)); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); + + // WHEN + defaultTbContext.output(msgMock, nodeConnection); + + // THEN + checkOutputCommonLogic(msgMock, nodeConnection); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + then(nodeCtxMock).shouldHaveNoMoreInteractions(); + } + + @ParameterizedTest + @ValueSource(strings = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE}) + void givenDebugStrategyAllEvents_whenOutput_thenVerifyDebugOutputPersisted(String nodeConnection) { + // GIVEN + var msgMock = mock(TbMsg.class); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.ALL_EVENTS); + given(msgMock.popFormStack()).willReturn(new TbMsgProcessingStackItem(RULE_CHAIN_ID, RULE_NODE_ID)); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); + mockGetMaxRuleNodeDebugModeDurationMinutes(); + + // WHEN + defaultTbContext.output(msgMock, nodeConnection); + + // THEN + checkOutputCommonLogic(msgMock, nodeConnection); + then(mainCtxMock).should().getTenantProfileCache(); + then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, nodeConnection); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + then(nodeCtxMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void givenEmptyStack_whenOutput_thenVerifyMsgAck() { + // GIVEN + var msgMock = mock(TbMsg.class); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.DISABLED); + given(msgMock.popFormStack()).willReturn(null); + TbMsgCallback callbackMock = mock(TbMsgCallback.class); + given(msgMock.getCallback()).willReturn(callbackMock); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + + // WHEN + defaultTbContext.output(msgMock, TbNodeConnectionType.SUCCESS); + + // THEN + then(msgMock).should().popFormStack(); + then(callbackMock).should().onProcessingEnd(RULE_NODE_ID); + then(callbackMock).should().onSuccess(); + then(nodeCtxMock).should(never()).getChainActor(); + } + + @Test + public void givenEmptyStackAndDebugStrategyAllEvents_whenOutput_thenVerifyMsgAckAndDebugOutputPersisted() { + // GIVEN + var msgMock = mock(TbMsg.class); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode.setLastUpdateTs(System.currentTimeMillis()); + given(msgMock.popFormStack()).willReturn(null); + TbMsgCallback callbackMock = mock(TbMsgCallback.class); + given(msgMock.getCallback()).willReturn(callbackMock); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + mockGetMaxRuleNodeDebugModeDurationMinutes(); + + // WHEN + defaultTbContext.output(msgMock, TbNodeConnectionType.SUCCESS); + + // THEN + then(msgMock).should().popFormStack(); + then(callbackMock).should().onProcessingEnd(RULE_NODE_ID); + then(callbackMock).should().onSuccess(); + then(nodeCtxMock).should(never()).getChainActor(); + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, TbNodeConnectionType.ACK); + } + + @Test + public void givenDebugStrategyOnlyFailureEvents_whenEnqueueForTellFailure_thenVerifyDebugOutputPersisted() { + // GIVEN + var msg = getTbMsgWithQueueName(); + var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.ONLY_FAILURE_EVENTS); + var tbClusterServiceMock = mock(TbClusterService.class); + + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi); + given(mainCtxMock.getClusterService()).willReturn(tbClusterServiceMock); + + // WHEN + defaultTbContext.enqueueForTellFailure(msg, EXCEPTION); + + // THEN + then(mainCtxMock).should().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, TENANT_ID, TENANT_ID); + TbMsg expectedTbMsg = TbMsg.newMsg(msg, msg.getQueueName(), RULE_CHAIN_ID, RULE_NODE_ID); + checkEnqueueForTellFailurePushMsgToRuleEngine(tbClusterServiceMock, tpi, expectedTbMsg); + ArgumentCaptor tbMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(mainCtxMock).should().persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), tbMsgCaptor.capture(), eq(TbNodeConnectionType.FAILURE), isNull(), eq(EXCEPTION_MSG)); + TbMsg actualTbMsg = tbMsgCaptor.getValue(); + assertThat(actualTbMsg).usingRecursiveComparison() + .ignoringFields("id", "ctx") + .isEqualTo(expectedTbMsg); + then(mainCtxMock).should().getClusterService(); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + then(tbClusterServiceMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void givenDebugStrategyDisabled_whenEnqueueForTellFailure_thenVerifyDebugOutputNotPersisted() { + // GIVEN + var msg = getTbMsgWithQueueName(); + var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.DISABLED); + var tbClusterServiceMock = mock(TbClusterService.class); + + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi); + given(mainCtxMock.getClusterService()).willReturn(tbClusterServiceMock); + + // WHEN + defaultTbContext.enqueueForTellFailure(msg, EXCEPTION); + + // THEN + then(mainCtxMock).should().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, TENANT_ID, TENANT_ID); + TbMsg expectedTbMsg = TbMsg.newMsg(msg, msg.getQueueName(), RULE_CHAIN_ID, RULE_NODE_ID); + checkEnqueueForTellFailurePushMsgToRuleEngine(tbClusterServiceMock, tpi, expectedTbMsg); + then(mainCtxMock).should().getClusterService(); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + then(tbClusterServiceMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void givenDebugStrategyAllEvents_whenEnqueueForTellFailure_thenVerifyDebugOutputPersisted() { + // GIVEN + var msg = getTbMsgWithQueueName(); + var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode.setLastUpdateTs(System.currentTimeMillis()); + var tbClusterServiceMock = mock(TbClusterService.class); + + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi); + given(mainCtxMock.getClusterService()).willReturn(tbClusterServiceMock); + mockGetMaxRuleNodeDebugModeDurationMinutes(); + + // WHEN + defaultTbContext.enqueueForTellFailure(msg, EXCEPTION); + + // THEN + then(mainCtxMock).should().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, TENANT_ID, TENANT_ID); + TbMsg expectedTbMsg = TbMsg.newMsg(msg, msg.getQueueName(), RULE_CHAIN_ID, RULE_NODE_ID); + checkEnqueueForTellFailurePushMsgToRuleEngine(tbClusterServiceMock, tpi, expectedTbMsg); + ArgumentCaptor tbMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(mainCtxMock).should().persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), tbMsgCaptor.capture(), eq(TbNodeConnectionType.FAILURE), isNull(), eq(EXCEPTION_MSG)); + TbMsg actualTbMsg = tbMsgCaptor.getValue(); + assertThat(actualTbMsg).usingRecursiveComparison() + .ignoringFields("id", "ctx") + .isEqualTo(expectedTbMsg); + then(mainCtxMock).should().getClusterService(); + then(mainCtxMock).should().getTenantProfileCache(); + then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + then(tbClusterServiceMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void givenInvalidMsg_whenEnqueueForTellFailure_thenDoNothing() { + // GIVEN + var msgMock = mock(TbMsg.class); + var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true); + + given(msgMock.getOriginator()).willReturn(TENANT_ID); + given(msgMock.getQueueName()).willReturn(DataConstants.MAIN_QUEUE_NAME); + given(msgMock.isValid()).willReturn(false); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi); + + // WHEN + defaultTbContext.enqueueForTellFailure(msgMock, EXCEPTION); + + // THEN + then(msgMock).should(times(2)).getQueueName(); + then(msgMock).should().getOriginator(); + then(msgMock).should().isValid(); + then(msgMock).shouldHaveNoMoreInteractions(); + + then(mainCtxMock).should().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, TENANT_ID, TENANT_ID); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + + then(nodeCtxMock).should(times(2)).getTenantId(); + then(nodeCtxMock).shouldHaveNoMoreInteractions(); + then(chainActorMock).shouldHaveNoInteractions(); + } + + @MethodSource + @ParameterizedTest + void givenDebugStrategyOptions_whenEnqueueForTellNext_thenVerifyDebugOutputPersistedOnlyForAllEventsDebugStrategy(DebugStrategy debugStrategy, String connectionType) { + // GIVEN + var msg = getTbMsgWithQueueName(); + var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(debugStrategy); + ruleNode.setLastUpdateTs(System.currentTimeMillis()); + var tbClusterServiceMock = mock(TbClusterService.class); + + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi); + given(mainCtxMock.getClusterService()).willReturn(tbClusterServiceMock); + if (DebugStrategy.ALL_EVENTS.equals(debugStrategy)) { + mockGetMaxRuleNodeDebugModeDurationMinutes(); + } + + // WHEN + defaultTbContext.enqueueForTellNext(msg, connectionType); + + // THEN + then(mainCtxMock).should().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, TENANT_ID, TENANT_ID); + TbMsg expectedTbMsg = TbMsg.newMsg(msg, msg.getQueueName(), RULE_CHAIN_ID, RULE_NODE_ID); + + ArgumentCaptor toRuleEngineMsgCaptor = ArgumentCaptor.forClass(ToRuleEngineMsg.class); + ArgumentCaptor simpleTbQueueCallbackCaptor = ArgumentCaptor.forClass(SimpleTbQueueCallback.class); + then(tbClusterServiceMock).should().pushMsgToRuleEngine(eq(tpi), notNull(UUID.class), toRuleEngineMsgCaptor.capture(), simpleTbQueueCallbackCaptor.capture()); + + ToRuleEngineMsg actualToRuleEngineMsg = toRuleEngineMsgCaptor.getValue(); + assertThat(actualToRuleEngineMsg).usingRecursiveComparison() + .ignoringFields("tbMsg_") + .isEqualTo(ToRuleEngineMsg.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setTbMsg(TbMsg.toByteString(expectedTbMsg)) + .addAllRelationTypes(List.of(connectionType)).build()); + + var simpleTbQueueCallback = simpleTbQueueCallbackCaptor.getValue(); + assertThat(simpleTbQueueCallback).isNotNull(); + simpleTbQueueCallback.onSuccess(null); + + if (DebugStrategy.ALL_EVENTS.equals(debugStrategy)) { + then(mainCtxMock).should().getTenantProfileCache(); + then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); + ArgumentCaptor tbMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); + then(mainCtxMock).should().persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), tbMsgCaptor.capture(), eq(connectionType), isNull(), isNull()); + TbMsg actualTbMsg = tbMsgCaptor.getValue(); + assertThat(actualTbMsg).usingRecursiveComparison() + .ignoringFields("id", "ctx") + .isEqualTo(expectedTbMsg); + } + then(mainCtxMock).should().getClusterService(); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + then(tbClusterServiceMock).shouldHaveNoMoreInteractions(); + } + + @MethodSource + @ParameterizedTest + void givenDebugStrategyOptions_whenEnqueue_thenVerifyDebugOutputPersistedOnlyForAllEventsDebugStrategy(DebugStrategy debugStrategy) { + // GIVEN + var msg = getTbMsgWithQueueName(); + var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setQueueName(DataConstants.MAIN_QUEUE_NAME); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(debugStrategy); + ruleNode.setLastUpdateTs(System.currentTimeMillis()); + var tbClusterServiceMock = mock(TbClusterService.class); + + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi); + given(mainCtxMock.getClusterService()).willReturn(tbClusterServiceMock); + if (DebugStrategy.ALL_EVENTS.equals(debugStrategy)) { + mockGetMaxRuleNodeDebugModeDurationMinutes(); + } + + Consumer onFailure = mock(Consumer.class); + Runnable onSuccess = mock(Runnable.class); + + // WHEN + defaultTbContext.enqueue(msg, onSuccess, onFailure); + + // THEN + then(mainCtxMock).should().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.MAIN_QUEUE_NAME, TENANT_ID, TENANT_ID); + TbMsg expectedTbMsg = TbMsg.newMsg(msg, msg.getQueueName(), RULE_CHAIN_ID, RULE_NODE_ID); + + ArgumentCaptor toRuleEngineMsgCaptor = ArgumentCaptor.forClass(ToRuleEngineMsg.class); + ArgumentCaptor simpleTbQueueCallbackCaptor = ArgumentCaptor.forClass(SimpleTbQueueCallback.class); + then(tbClusterServiceMock).should().pushMsgToRuleEngine(eq(tpi), notNull(UUID.class), toRuleEngineMsgCaptor.capture(), simpleTbQueueCallbackCaptor.capture()); + + ToRuleEngineMsg actualToRuleEngineMsg = toRuleEngineMsgCaptor.getValue(); + assertThat(actualToRuleEngineMsg).usingRecursiveComparison() + .ignoringFields("tbMsg_") + .isEqualTo(ToRuleEngineMsg.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setTbMsg(TbMsg.toByteString(expectedTbMsg)) + .build()); + + var simpleTbQueueCallback = simpleTbQueueCallbackCaptor.getValue(); + assertThat(simpleTbQueueCallback).isNotNull(); + simpleTbQueueCallback.onSuccess(null); + + if (DebugStrategy.ALL_EVENTS.equals(debugStrategy)) { + then(mainCtxMock).should().getTenantProfileCache(); + then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); + then(mainCtxMock).should().persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), eq(msg), eq(TbNodeConnectionType.TO_ROOT_RULE_CHAIN)); + } + then(mainCtxMock).should().getClusterService(); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + then(tbClusterServiceMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void givenDebugStrategyOnlyFailures_whenTellFailure_thenVerifyDebugOutputPersisted() { + // GIVEN + var msg = getTbMsg(); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.ONLY_FAILURE_EVENTS); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); + + // WHEN + defaultTbContext.tellFailure(msg, EXCEPTION); + + // THEN + var expectedRuleNodeToRuleChainTellNextMsg = new RuleNodeToRuleChainTellNextMsg( + RULE_CHAIN_ID, + RULE_NODE_ID, + Collections.singleton(TbNodeConnectionType.FAILURE), + msg, + EXCEPTION_MSG + ); + then(chainActorMock).should().tell(expectedRuleNodeToRuleChainTellNextMsg); + then(chainActorMock).shouldHaveNoMoreInteractions(); + then(nodeCtxMock).should().getChainActor(); + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, TbNodeConnectionType.FAILURE, EXCEPTION); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + then(nodeCtxMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void givenDebugStrategyDisabled_whenTellFailure_thenVerifyDebugOutputNotPersisted() { + // GIVEN + var msg = getTbMsg(); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.DISABLED); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); + + // WHEN + defaultTbContext.tellFailure(msg, EXCEPTION); + + // THEN + var expectedRuleNodeToRuleChainTellNextMsg = new RuleNodeToRuleChainTellNextMsg( + RULE_CHAIN_ID, + RULE_NODE_ID, + Collections.singleton(TbNodeConnectionType.FAILURE), + msg, + EXCEPTION_MSG + ); + then(chainActorMock).should().tell(expectedRuleNodeToRuleChainTellNextMsg); + then(chainActorMock).shouldHaveNoMoreInteractions(); + then(nodeCtxMock).should().getChainActor(); + then(mainCtxMock).shouldHaveNoInteractions(); + then(nodeCtxMock).shouldHaveNoMoreInteractions(); + } + + @Test + public void givenDebugStrategyAllEvents_whenTellFailure_thenVerifyDebugOutputPersisted() { + // GIVEN + var msg = getTbMsg(); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode.setLastUpdateTs(System.currentTimeMillis()); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); + mockGetMaxRuleNodeDebugModeDurationMinutes(); + + // WHEN + defaultTbContext.tellFailure(msg, EXCEPTION); + + // THEN + var expectedRuleNodeToRuleChainTellNextMsg = new RuleNodeToRuleChainTellNextMsg( + RULE_CHAIN_ID, + RULE_NODE_ID, + Collections.singleton(TbNodeConnectionType.FAILURE), + msg, + EXCEPTION_MSG + ); + then(chainActorMock).should().tell(expectedRuleNodeToRuleChainTellNextMsg); + then(chainActorMock).shouldHaveNoMoreInteractions(); + then(nodeCtxMock).should().getChainActor(); + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, TbNodeConnectionType.FAILURE, EXCEPTION); + then(mainCtxMock).should().getTenantProfileCache(); + then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + then(nodeCtxMock).shouldHaveNoMoreInteractions(); + } + + private void checkTellNextCommonLogic(TbMsgCallback callbackMock, String nodeConnection, TbMsg msg) { + checkTellNextCommonLogic(callbackMock, Collections.singleton(nodeConnection), msg); + } + + private void checkTellNextCommonLogic(TbMsgCallback callbackMock, Set nodeConnections, TbMsg msg) { + then(callbackMock).should().onProcessingEnd(RULE_NODE_ID); + then(callbackMock).shouldHaveNoMoreInteractions(); + var expectedRuleNodeToRuleChainTellNextMsg = new RuleNodeToRuleChainTellNextMsg( + RULE_CHAIN_ID, + RULE_NODE_ID, + nodeConnections, + msg, + null); + then(chainActorMock).should().tell(expectedRuleNodeToRuleChainTellNextMsg); + then(chainActorMock).shouldHaveNoMoreInteractions(); + } + + private void checkOutputCommonLogic(TbMsg msg, String nodeConnection) { + then(msg).should().popFormStack(); + var expectedRuleChainOutputMsg = new RuleChainOutputMsg( + RULE_CHAIN_ID, + RULE_NODE_ID, + nodeConnection, + msg); + then(chainActorMock).should().tell(expectedRuleChainOutputMsg); + then(chainActorMock).shouldHaveNoMoreInteractions(); + then(nodeCtxMock).should().getChainActor(); + } + + private void checkEnqueueForTellFailurePushMsgToRuleEngine(TbClusterService tbClusterService, TopicPartitionInfo tpi, TbMsg expectedTbMsg) { + ArgumentCaptor toRuleEngineMsgCaptor = ArgumentCaptor.forClass(ToRuleEngineMsg.class); + ArgumentCaptor simpleTbQueueCallbackCaptor = ArgumentCaptor.forClass(SimpleTbQueueCallback.class); + then(tbClusterService).should().pushMsgToRuleEngine(eq(tpi), notNull(UUID.class), toRuleEngineMsgCaptor.capture(), simpleTbQueueCallbackCaptor.capture()); + + ToRuleEngineMsg actualToRuleEngineMsg = toRuleEngineMsgCaptor.getValue(); + assertThat(actualToRuleEngineMsg).usingRecursiveComparison() + .ignoringFields("tbMsg_") + .isEqualTo(ToRuleEngineMsg.newBuilder() + .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) + .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) + .setTbMsg(TbMsg.toByteString(expectedTbMsg)) + .setFailureMessage(EXCEPTION_MSG) + .addAllRelationTypes(List.of(TbNodeConnectionType.FAILURE)).build()); + + var simpleTbQueueCallback = simpleTbQueueCallbackCaptor.getValue(); + assertThat(simpleTbQueueCallback).isNotNull(); + simpleTbQueueCallback.onSuccess(null); + } + + private static Stream givenDebugStrategyOptions_whenEnqueueForTellNext_thenVerifyDebugOutputPersistedOnlyForAllEventsDebugStrategy() { + return Stream.of( + Arguments.of(DebugStrategy.ALL_EVENTS, TbNodeConnectionType.OTHER), + Arguments.of(DebugStrategy.ONLY_FAILURE_EVENTS, TbNodeConnectionType.TRUE), + Arguments.of(DebugStrategy.DISABLED, TbNodeConnectionType.FALSE) + ); + } + + private static Stream givenDebugStrategyOptions_whenEnqueue_thenVerifyDebugOutputPersistedOnlyForAllEventsDebugStrategy() { + return Stream.of( + Arguments.of(DebugStrategy.ALL_EVENTS), + Arguments.of(DebugStrategy.ONLY_FAILURE_EVENTS), + Arguments.of(DebugStrategy.DISABLED) + ); + } + + private TbMsg getTbMsgWithCallback(TbMsgCallback callback) { + return TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TENANT_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING, callback); + } + + private TbMsg getTbMsgWithQueueName() { + return TbMsg.newMsg(DataConstants.MAIN_QUEUE_NAME, TbMsgType.POST_TELEMETRY_REQUEST, TENANT_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + } + + private TbMsg getTbMsg() { + return TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TENANT_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); + } + + private void mockGetMaxRuleNodeDebugModeDurationMinutes() { + var tbTenantProfileCacheMock = mock(TbTenantProfileCache.class); + var tenantProfileMock = mock(TenantProfile.class); + var tenantProfileDataMock = mock(TenantProfileData.class); + var tenantProfileConfigurationMock = mock(TenantProfileConfiguration.class); + + given(mainCtxMock.getTenantProfileCache()).willReturn(tbTenantProfileCacheMock); + given(tbTenantProfileCacheMock.get(TENANT_ID)).willReturn(tenantProfileMock); + given(tenantProfileMock.getProfileData()).willReturn(tenantProfileDataMock); + given(tenantProfileDataMock.getConfiguration()).willReturn(tenantProfileConfigurationMock); + given(tenantProfileConfigurationMock.getMaxRuleNodeDebugModeDurationMinutes(anyInt())).willReturn(15); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index 5484c29305..a09ca1d148 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -71,6 +71,7 @@ import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -103,7 +104,6 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.Random; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -125,8 +125,6 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { protected EdgeImitator edgeImitator; protected Edge edge; - private Random random = new Random(); - @Autowired protected EdgeEventService edgeEventService; @@ -210,7 +208,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { protected void updateRootRuleChainMetadata() throws Exception { RuleChainId rootRuleChainId = getEdgeRootRuleChainId(); RuleChainMetaData rootRuleChainMetadata = doGet("/api/ruleChain/" + rootRuleChainId.getId().toString() + "/metadata", RuleChainMetaData.class); - rootRuleChainMetadata.getNodes().forEach(n -> n.setDebugMode(random.nextBoolean())); + rootRuleChainMetadata.getNodes().forEach(n -> n.setDebugStrategy(DebugStrategy.ALL_EVENTS)); doPost("/api/ruleChain/metadata", rootRuleChainMetadata, RuleChainMetaData.class); } diff --git a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java index eaf4468156..2dc2bd42f3 100644 --- a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java @@ -23,6 +23,7 @@ import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -228,7 +229,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { // update metadata for root rule chain edgeImitator.expectMessageAmount(1); - metaData.getNodes().forEach(n -> n.setDebugMode(true)); + metaData.getNodes().forEach(n -> n.setDebugStrategy(DebugStrategy.ALL_EVENTS)); doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class); Assert.assertTrue(edgeImitator.waitForMessages()); ruleChainUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainUpdateMsg.class); diff --git a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java index 9e36fed677..3af4c012e0 100644 --- a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java @@ -43,6 +43,7 @@ import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; @@ -142,7 +143,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule ruleNode1.setName("Simple Rule Node 1"); ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode1.setDebugMode(true); + ruleNode1.setDebugStrategy(DebugStrategy.ALL_EVENTS); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); configuration1.setFetchTo(TbMsgSource.METADATA); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); @@ -152,7 +153,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode2.setDebugMode(true); + ruleNode2.setDebugStrategy(DebugStrategy.ALL_EVENTS); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); configuration2.setFetchTo(TbMsgSource.METADATA); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); @@ -248,7 +249,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule ruleNode1.setName("Simple Rule Node 1"); ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode1.setDebugMode(true); + ruleNode1.setDebugStrategy(DebugStrategy.ALL_EVENTS); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); configuration1.setFetchTo(TbMsgSource.METADATA); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); @@ -257,7 +258,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule RuleNode ruleNode12 = new RuleNode(); ruleNode12.setName("Simple Rule Node 1"); ruleNode12.setType(org.thingsboard.rule.engine.flow.TbRuleChainInputNode.class.getName()); - ruleNode12.setDebugMode(true); + ruleNode12.setDebugStrategy(DebugStrategy.ALL_EVENTS); TbRuleChainInputNodeConfiguration configuration12 = new TbRuleChainInputNodeConfiguration(); configuration12.setRuleChainId(secondaryRuleChain.getId().getId().toString()); ruleNode12.setConfiguration(JacksonUtil.valueToTree(configuration12)); @@ -282,7 +283,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode2.setDebugMode(true); + ruleNode2.setDebugStrategy(DebugStrategy.ALL_EVENTS); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); configuration2.setFetchTo(TbMsgSource.METADATA); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java index 4c4e887644..dd148748ca 100644 --- a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; @@ -97,7 +98,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac ruleNode.setName("Simple Rule Node"); ruleNode.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode.setDebugMode(true); + ruleNode.setDebugStrategy(DebugStrategy.ALL_EVENTS); TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration(); configuration.setFetchTo(TbMsgSource.METADATA); configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey")); diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index d3b1916def..752624fa51 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -58,6 +58,7 @@ import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -462,7 +463,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { ruleNode1.setName("Simple Rule Node 1"); ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode1.setDebugMode(true); + ruleNode1.setDebugStrategy(DebugStrategy.ALL_EVENTS); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1)); @@ -471,7 +472,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode2.setDebugMode(true); + ruleNode2.setDebugStrategy(DebugStrategy.ALL_EVENTS); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index 55f15516f4..06bc670804 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -65,6 +65,7 @@ import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -471,7 +472,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { RuleNode ruleNode1 = new RuleNode(); ruleNode1.setName("Generator 1"); ruleNode1.setType(TbMsgGeneratorNode.class.getName()); - ruleNode1.setDebugMode(true); + ruleNode1.setDebugStrategy(DebugStrategy.ALL_EVENTS); TbMsgGeneratorNodeConfiguration configuration1 = new TbMsgGeneratorNodeConfiguration(); configuration1.setOriginatorType(originatorId.getEntityType()); configuration1.setOriginatorId(originatorId.getId().toString()); @@ -481,7 +482,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode2.setDebugMode(true); + ruleNode2.setDebugStrategy(DebugStrategy.ALL_EVENTS); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); @@ -510,7 +511,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { ruleNode1.setName("Simple Rule Node 1"); ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode1.setDebugMode(true); + ruleNode1.setDebugStrategy(DebugStrategy.ALL_EVENTS); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1)); @@ -519,7 +520,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode2.setDebugMode(true); + ruleNode2.setDebugStrategy(DebugStrategy.ALL_EVENTS); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index 2e01d700a9..21f6363f5f 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -65,6 +65,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -870,7 +871,7 @@ public class VersionControlTest extends AbstractControllerTest { RuleNode ruleNode1 = new RuleNode(); ruleNode1.setName("Generator 1"); ruleNode1.setType(TbMsgGeneratorNode.class.getName()); - ruleNode1.setDebugMode(true); + ruleNode1.setDebugStrategy(DebugStrategy.ALL_EVENTS); TbMsgGeneratorNodeConfiguration configuration1 = new TbMsgGeneratorNodeConfiguration(); configuration1.setOriginatorType(originatorId.getEntityType()); configuration1.setOriginatorId(originatorId.getId().toString()); @@ -880,7 +881,7 @@ public class VersionControlTest extends AbstractControllerTest { ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode2.setDebugMode(true); + ruleNode2.setDebugStrategy(DebugStrategy.ALL_EVENTS); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); @@ -908,7 +909,7 @@ public class VersionControlTest extends AbstractControllerTest { ruleNode1.setName("Simple Rule Node 1"); ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode1.setDebugMode(true); + ruleNode1.setDebugStrategy(DebugStrategy.ALL_EVENTS); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1)); @@ -917,7 +918,7 @@ public class VersionControlTest extends AbstractControllerTest { ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode2.setDebugMode(true); + ruleNode2.setDebugStrategy(DebugStrategy.ALL_EVENTS); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbNodeConnectionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbNodeConnectionType.java index 2f466c76b0..a6d56ec221 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbNodeConnectionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/msg/TbNodeConnectionType.java @@ -23,9 +23,13 @@ public final class TbNodeConnectionType { public static final String SUCCESS = "Success"; public static final String FAILURE = "Failure"; + public static final String ACK = "ACK"; + public static final String TRUE = "True"; public static final String FALSE = "False"; public static final String OTHER = "Other"; + public static final String TO_ROOT_RULE_CHAIN = "To Root Rule Chain"; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/DebugStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DebugStrategy.java new file mode 100644 index 0000000000..333ee9c670 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DebugStrategy.java @@ -0,0 +1,72 @@ +/** + * Copyright © 2016-2024 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.rule; + +import lombok.Getter; +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; + +import java.util.Set; +import java.util.concurrent.TimeUnit; + +@Getter +public enum DebugStrategy { + DISABLED(0), ALL_EVENTS(1), ONLY_FAILURE_EVENTS(2); + + private final int protoNumber; + + DebugStrategy(int protoNumber) { + this.protoNumber = protoNumber; + } + + public boolean shouldPersistDebugInput(long lastUpdateTs, long msgTs, int debugModeDurationMinutes) { + return isAllEventsStrategyAndMsgTsWithinDebugDuration(lastUpdateTs, msgTs, debugModeDurationMinutes); + } + + public boolean shouldPersistDebugOutputForAllEvents(long lastUpdateTs, long msgTs, int debugModeDurationMinutes) { + return isAllEventsStrategyAndMsgTsWithinDebugDuration(lastUpdateTs, msgTs, debugModeDurationMinutes); + } + + public boolean shouldPersistDebugForFailureEventOnly(long lastUpdateTs, long msgTs, int debugModeDurationMinutes) { + return shouldPersistDebugOutputForAllEvents(lastUpdateTs, msgTs, debugModeDurationMinutes) || isFailureOnlyStrategy(); + } + + public boolean shouldPersistDebugForFailureEventOnly(Set nodeConnections) { + return isFailureOnlyStrategy() && nodeConnections.contains(TbNodeConnectionType.FAILURE); + } + + public boolean shouldPersistDebugForFailureEventOnly(String nodeConnection) { + return isFailureOnlyStrategy() && TbNodeConnectionType.FAILURE.equals(nodeConnection); + } + + private boolean isFailureOnlyStrategy() { + return DebugStrategy.ONLY_FAILURE_EVENTS.equals(this); + } + + private boolean isAllEventsStrategyAndMsgTsWithinDebugDuration(long lastUpdateTs, long msgTs, int debugModeDurationMinutes) { + if (!DebugStrategy.ALL_EVENTS.equals(this)) { + return false; + } + return isMsgTsWithinDebugDuration(lastUpdateTs, msgTs, debugModeDurationMinutes); + } + + private boolean isMsgTsWithinDebugDuration(long lastUpdateTs, long msgCreationTs, int debugModeDurationMinutes) { + if (debugModeDurationMinutes <= 0) { + return true; + } + return msgCreationTs < lastUpdateTs + TimeUnit.MINUTES.toMillis(debugModeDurationMinutes); + } + +} 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 542debcfdd..c147da1359 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 @@ -45,15 +45,17 @@ public class RuleNode extends BaseDataWithAdditionalInfo implements @Length(fieldName = "name") @Schema(description = "User defined name of the rule node. Used on UI and for logging. ", example = "Process sensor reading") private String name; - @Schema(description = "Enable/disable debug. ", example = "false") - private boolean debugMode; + @Schema(description = "Timestamp of the last rule node update.") + private long lastUpdateTs; + @Schema(description = "Debug strategy. ", example = "ALL_EVENTS") + private DebugStrategy debugStrategy; @Schema(description = "Enable/disable singleton mode. ", example = "false") private boolean singletonMode; @Schema(description = "Queue name. ", example = "Main") private String queueName; @Schema(description = "Version of rule node configuration. ", example = "0") private int configurationVersion; - @Schema(description = "JSON with the rule node configuration. Structure depends on the rule node implementation.", implementation = com.fasterxml.jackson.databind.JsonNode.class) + @Schema(description = "JSON with the rule node configuration. Structure depends on the rule node implementation.", implementation = JsonNode.class) private transient JsonNode configuration; @JsonIgnore private byte[] configurationBytes; @@ -73,7 +75,8 @@ public class RuleNode extends BaseDataWithAdditionalInfo implements this.ruleChainId = ruleNode.getRuleChainId(); this.type = ruleNode.getType(); this.name = ruleNode.getName(); - this.debugMode = ruleNode.isDebugMode(); + this.lastUpdateTs = ruleNode.getLastUpdateTs(); + this.debugStrategy = ruleNode.getDebugStrategy(); this.singletonMode = ruleNode.isSingletonMode(); this.setConfiguration(ruleNode.getConfiguration()); this.externalId = ruleNode.getExternalId(); @@ -84,6 +87,10 @@ public class RuleNode extends BaseDataWithAdditionalInfo implements return name; } + public DebugStrategy getDebugStrategy() { + return debugStrategy == null ? DebugStrategy.DISABLED : debugStrategy; + } + public JsonNode getConfiguration() { return BaseDataWithAdditionalInfo.getJson(() -> configuration, () -> configurationBytes); } @@ -93,9 +100,9 @@ public class RuleNode extends BaseDataWithAdditionalInfo implements } @Schema(description = "JSON object with the Rule Node Id. " + - "Specify this field to update the Rule Node. " + - "Referencing non-existing Rule Node Id will cause error. " + - "Omit this field to create new rule node.") + "Specify this field to update the Rule Node. " + + "Referencing non-existing Rule Node Id will cause error. " + + "Omit this field to create new rule node.") @Override public RuleNodeId getId() { return super.getId(); @@ -107,10 +114,9 @@ public class RuleNode extends BaseDataWithAdditionalInfo implements return super.getCreatedTime(); } - @Schema(description = "Additional parameters of the rule node. Contains 'layoutX' and 'layoutY' properties for visualization.", implementation = com.fasterxml.jackson.databind.JsonNode.class) + @Schema(description = "Additional parameters of the rule node. Contains 'layoutX' and 'layoutY' properties for visualization.", implementation = JsonNode.class) @Override public JsonNode getAdditionalInfo() { return super.getAdditionalInfo(); } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 51738c75be..ff5c0ea426 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -24,6 +24,8 @@ import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.TenantProfileType; +import java.io.Serial; + @Schema @AllArgsConstructor @NoArgsConstructor @@ -31,6 +33,7 @@ import org.thingsboard.server.common.data.TenantProfileType; @Data public class DefaultTenantProfileConfiguration implements TenantProfileConfiguration { + @Serial private static final long serialVersionUID = -7134932690332578595L; private long maxDevices; @@ -91,6 +94,8 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxDPStorageDays; @Schema(example = "50") private int maxRuleNodeExecutionsPerMessage; + @Schema(example = "15") + private int maxRuleNodeDebugDurationMinutes; @Schema(example = "0") private long maxEmails; @Schema(example = "true") @@ -197,4 +202,9 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura public int getMaxRuleNodeExecsPerMessage() { return maxRuleNodeExecutionsPerMessage; } + + @Override + public int getMaxRuleNodeDebugModeDurationMinutes(int systemMaxRuleNodeDebugModeDurationMinutes) { + return Math.min(systemMaxRuleNodeDebugModeDurationMinutes, maxRuleNodeDebugDurationMinutes); + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java index 0662765455..af36a15c4f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/TenantProfileConfiguration.java @@ -51,4 +51,7 @@ public interface TenantProfileConfiguration extends Serializable { @JsonIgnore int getMaxRuleNodeExecsPerMessage(); + @JsonIgnore + int getMaxRuleNodeDebugModeDurationMinutes(int systemMaxRuleNodeDebugModeDurationMinutes); + } diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 5fb51de2ab..f5765f3103 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -162,17 +162,25 @@ message RuleChainMetadataUpdateMsg { string entity = 8; } +enum DebugStrategy { + DISABLED = 0; + ALL_EVENTS = 1; + ONLY_FAILURE_EVENTS = 2; +} + message RuleNodeProto { option deprecated = true; int64 idMSB = 1; int64 idLSB = 2; string type = 3; string name = 4; - bool debugMode = 5; + bool debugMode = 5 [deprecated = true]; string configuration = 6; string additionalInfo = 7; bool singletonMode = 8; int32 configurationVersion = 9; + int64 lastUpdateTs = 10; + DebugStrategy debugStrategy = 11; } message NodeConnectionInfoProto { 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 b920160cc0..a7da9bf516 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 @@ -397,6 +397,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 DEBUG_STRATEGY = "debug_strategy"; public static final String SINGLETON_MODE = "singleton_mode"; public static final String QUEUE_NAME = "queue_name"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java index bdba4549bb..278fde0d6d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java @@ -19,11 +19,14 @@ import com.fasterxml.jackson.databind.JsonNode; import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.BaseSqlEntity; @@ -58,8 +61,12 @@ public class RuleNodeEntity extends BaseSqlEntity { @Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY) private JsonNode additionalInfo; - @Column(name = ModelConstants.DEBUG_MODE) - private boolean debugMode; + @Column(name = ModelConstants.LAST_UPDATE_TS_COLUMN) + private long lastUpdateTs; + + @Enumerated(EnumType.STRING) + @Column(name = ModelConstants.DEBUG_STRATEGY) + private DebugStrategy debugStrategy; @Column(name = ModelConstants.SINGLETON_MODE) private boolean singletonMode; @@ -83,7 +90,8 @@ public class RuleNodeEntity extends BaseSqlEntity { } this.type = ruleNode.getType(); this.name = ruleNode.getName(); - this.debugMode = ruleNode.isDebugMode(); + this.lastUpdateTs = ruleNode.getLastUpdateTs(); + this.debugStrategy = ruleNode.getDebugStrategy(); this.singletonMode = ruleNode.isSingletonMode(); this.queueName = ruleNode.getQueueName(); this.configurationVersion = ruleNode.getConfigurationVersion(); @@ -103,7 +111,8 @@ public class RuleNodeEntity extends BaseSqlEntity { } ruleNode.setType(type); ruleNode.setName(name); - ruleNode.setDebugMode(debugMode); + ruleNode.setLastUpdateTs(lastUpdateTs); + ruleNode.setDebugStrategy(debugStrategy); ruleNode.setSingletonMode(singletonMode); ruleNode.setQueueName(queueName); ruleNode.setConfigurationVersion(configurationVersion); 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 6f34437e0d..f141ef627e 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 @@ -44,6 +44,7 @@ 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.DebugStrategy; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; @@ -214,9 +215,11 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } RuleChainId ruleChainId = ruleChain.getId(); if (nodes != null) { + long lastUpdateTs = System.currentTimeMillis(); for (RuleNode node : toAddOrUpdate) { node.setRuleChainId(ruleChainId); node = ruleNodeUpdater.apply(node); + node.setLastUpdateTs(lastUpdateTs); RuleChainDataValidator.validateRuleNode(node); RuleNode savedNode = ruleNodeDao.save(tenantId, node); relations.add(new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(), @@ -261,7 +264,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC layout.remove("description"); layout.remove("ruleChainNodeId"); targetNode.setAdditionalInfo(layout); - targetNode.setDebugMode(false); + targetNode.setDebugStrategy(DebugStrategy.DISABLED); targetNode = ruleNodeDao.save(tenantId, targetNode); EntityRelation sourceRuleChainToRuleNode = new EntityRelation(); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 9c95f385f8..9772736191 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -193,7 +193,8 @@ CREATE TABLE IF NOT EXISTS rule_node ( configuration varchar(10000000), type varchar(255), name varchar(255), - debug_mode boolean, + last_update_ts bigint NOT NULL, + debug_strategy varchar(32) DEFAULT 'DISABLED', singleton_mode boolean, queue_name varchar(255), external_id uuid From 3b491c978feb9a01d17b5de21cab56ef94676116 Mon Sep 17 00:00:00 2001 From: "dong.zhou" Date: Tue, 15 Oct 2024 16:36:01 +0800 Subject: [PATCH 06/77] fix rest-client getTenantDeviceInfos by active search --- .../main/java/org/thingsboard/rest/client/RestClient.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 818d714a3b..0f1d1a802a 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -1356,13 +1356,14 @@ public class RestClient implements Closeable { }, params).getBody(); } - public PageData getTenantDeviceInfos(String type, DeviceProfileId deviceProfileId, PageLink pageLink) { + public PageData getTenantDeviceInfos(String type, Boolean active, DeviceProfileId deviceProfileId, PageLink pageLink) { Map params = new HashMap<>(); params.put("type", type); + params.put("active", active != null ? active.toString() : null); params.put("deviceProfileId", deviceProfileId != null ? deviceProfileId.toString() : null); addPageLinkToParam(params, pageLink); return restTemplate.exchange( - baseURL + "/api/tenant/deviceInfos?type={type}&deviceProfileId={deviceProfileId}&" + getUrlParams(pageLink), + baseURL + "/api/tenant/deviceInfos?type={type}&active={active}&deviceProfileId={deviceProfileId}&" + getUrlParams(pageLink), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { }, params).getBody(); From 54a7400611c0c5ea8ad5408fda004f7a19397086 Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Tue, 15 Oct 2024 14:39:34 +0300 Subject: [PATCH 07/77] Fixed stored rule chain json. Added ignore uknown properties for rule node class --- .../device_profile/rule_chain_template.json | 14 ++++---- .../server/common/data/rule/RuleNode.java | 2 ++ .../src/main/resources/root_rule_chain.json | 36 +++++++++---------- .../resources/MqttRuleNodeTestMetadata.json | 6 ++-- .../RpcResponseRuleChainMetadata.json | 6 ++-- 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json index 0256a2ccf2..b005331862 100644 --- a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json +++ b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json @@ -19,7 +19,7 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "defaultTTL": 0 } @@ -31,7 +31,7 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", - "debugMode": false, + "debugStrategy": "DISABLED", "configurationVersion": 2, "configuration": { "scope": "CLIENT_SCOPE", @@ -47,7 +47,7 @@ }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "Message Type Switch", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "version": 0 } @@ -59,7 +59,7 @@ }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log RPC from Device", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "scriptLang": "TBEL", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", @@ -73,7 +73,7 @@ }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log Other", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "scriptLang": "TBEL", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", @@ -87,7 +87,7 @@ }, "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "name": "RPC Call Request", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "timeoutInSeconds": 60 } @@ -100,7 +100,7 @@ }, "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", "name": "Device Profile Node", - "debugMode": false, + "debugStrategy": "DISABLED", "configuration": { "persistAlarmRulesState": false, "fetchAlarmRulesStateOnStart": false 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 c147da1359..35601afe02 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 @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.rule; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -32,6 +33,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @Data @EqualsAndHashCode(callSuper = true) @Slf4j +@JsonIgnoreProperties(ignoreUnknown = true) public class RuleNode extends BaseDataWithAdditionalInfo implements HasName { private static final long serialVersionUID = -5656679015121235465L; diff --git a/monitoring/src/main/resources/root_rule_chain.json b/monitoring/src/main/resources/root_rule_chain.json index ed8a93fb63..1da16c9b09 100644 --- a/monitoring/src/main/resources/root_rule_chain.json +++ b/monitoring/src/main/resources/root_rule_chain.json @@ -20,7 +20,7 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", - "debugMode": true, + "debugStrategy": "ALL_EVENTS", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -35,7 +35,7 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Attributes", - "debugMode": false, + "debugStrategy": "DISABLED", "singletonMode": false, "configurationVersion": 1, "configuration": { @@ -53,7 +53,7 @@ }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "Message Type Switch", - "debugMode": false, + "debugStrategy": "DISABLED", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -68,7 +68,7 @@ }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log RPC from Device", - "debugMode": false, + "debugStrategy": "DISABLED", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -85,7 +85,7 @@ }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log Other", - "debugMode": false, + "debugStrategy": "DISABLED", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -102,7 +102,7 @@ }, "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "name": "RPC Call Request", - "debugMode": false, + "debugStrategy": "DISABLED", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -117,7 +117,7 @@ }, "type": "org.thingsboard.rule.engine.filter.TbOriginatorTypeFilterNode", "name": "Is Entity Group", - "debugMode": false, + "debugStrategy": "DISABLED", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -134,7 +134,7 @@ }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeFilterNode", "name": "Post attributes or RPC request", - "debugMode": false, + "debugStrategy": "DISABLED", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -152,7 +152,7 @@ }, "type": "org.thingsboard.rule.engine.transform.TbDuplicateMsgToGroupNode", "name": "Duplicate To Group Entities", - "debugMode": false, + "debugStrategy": "DISABLED", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -169,7 +169,7 @@ }, "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", "name": "Device Profile Node", - "debugMode": true, + "debugStrategy": "ALL_EVENTS", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -186,7 +186,7 @@ }, "type": "org.thingsboard.rule.engine.filter.TbJsFilterNode", "name": "Test JS script", - "debugMode": false, + "debugStrategy": "DISABLED", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -204,7 +204,7 @@ }, "type": "org.thingsboard.rule.engine.filter.TbJsFilterNode", "name": "Test TBEL script", - "debugMode": false, + "debugStrategy": "DISABLED", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -222,7 +222,7 @@ }, "type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode", "name": "Add arrival timestamp", - "debugMode": false, + "debugStrategy": "DISABLED", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -240,7 +240,7 @@ }, "type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode", "name": "Calculate additional latencies", - "debugMode": true, + "debugStrategy": "ALL_EVENTS", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -258,7 +258,7 @@ }, "type": "org.thingsboard.rule.engine.transform.TbChangeOriginatorNode", "name": "To latencies asset", - "debugMode": false, + "debugStrategy": "DISABLED", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -287,7 +287,7 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", - "debugMode": true, + "debugStrategy": "ALL_EVENTS", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -303,7 +303,7 @@ }, "type": "org.thingsboard.rule.engine.filter.TbCheckMessageNode", "name": "Has testData", - "debugMode": false, + "debugStrategy": "DISABLED", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -323,7 +323,7 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries with TTL", - "debugMode": true, + "debugStrategy": "ALL_EVENTS", "singletonMode": false, "configurationVersion": 0, "configuration": { diff --git a/msa/black-box-tests/src/test/resources/MqttRuleNodeTestMetadata.json b/msa/black-box-tests/src/test/resources/MqttRuleNodeTestMetadata.json index 7a5015add3..dc6a394a39 100644 --- a/msa/black-box-tests/src/test/resources/MqttRuleNodeTestMetadata.json +++ b/msa/black-box-tests/src/test/resources/MqttRuleNodeTestMetadata.json @@ -9,7 +9,7 @@ }, "type": "org.thingsboard.rule.engine.mqtt.TbMqttNode", "name": "test mqtt", - "debugMode": true, + "debugStrategy": "ALL_EVENTS", "singletonMode": true, "queueName": "HighPriority", "configurationVersion": 0, @@ -36,7 +36,7 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "save timeseries", - "debugMode": true, + "debugStrategy": "ALL_EVENTS", "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -54,7 +54,7 @@ }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "switch", - "debugMode": false, + "debugStrategy": "DISABLED", "singletonMode": false, "configurationVersion": 0, "configuration": { diff --git a/msa/black-box-tests/src/test/resources/RpcResponseRuleChainMetadata.json b/msa/black-box-tests/src/test/resources/RpcResponseRuleChainMetadata.json index 09178ef781..31eb3149f3 100644 --- a/msa/black-box-tests/src/test/resources/RpcResponseRuleChainMetadata.json +++ b/msa/black-box-tests/src/test/resources/RpcResponseRuleChainMetadata.json @@ -8,7 +8,7 @@ }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "msgTypeSwitch", - "debugMode": true, + "debugStrategy": "ALL_EVENTS", "configuration": { "version": 0 } @@ -20,7 +20,7 @@ }, "type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode", "name": "formResponse", - "debugMode": true, + "debugStrategy": "ALL_EVENTS", "configuration": { "jsScript": "if (msg.method == \"getResponse\") {\n return {msg: {\"response\": \"requestReceived\"}, metadata: metadata, msgType: msgType};\n}\n\nreturn {msg: msg, metadata: metadata, msgType: msgType};" } @@ -32,7 +32,7 @@ }, "type": "org.thingsboard.rule.engine.rpc.TbSendRPCReplyNode", "name": "rpcReply", - "debugMode": true, + "debugStrategy": "ALL_EVENTS", "configuration": { "requestIdMetaDataAttribute": "requestId" } From b57bf2878179a346201046662f12480ac1846eeb Mon Sep 17 00:00:00 2001 From: Johan Ejdemark Date: Sat, 19 Oct 2024 22:22:18 +0200 Subject: [PATCH 08/77] Fix email sender address issue Fixes #9878 Update `TbMailSender` to correctly handle the `mailFrom` field in the email configuration. * Checks the `mailFrom` field in the JSON configuration and set it as the sender address if present. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/thingsboard/thingsboard/issues/9878?shareId=XXXX-XXXX-XXXX-XXXX). --- .../java/org/thingsboard/server/service/mail/TbMailSender.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java b/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java index 10586bc41a..93ac09b0fc 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/TbMailSender.java @@ -67,6 +67,9 @@ public class TbMailSender extends JavaMailSenderImpl { if (jsonConfig.has("password")) { setPassword(jsonConfig.get("password").asText()); } + if (jsonConfig.has("mailFrom")) { + setUsername(jsonConfig.get("mailFrom").asText()); + } setJavaMailProperties(createJavaMailProperties(jsonConfig)); } From cc977f42b2ddb46a82affb96226c41b4a426d199 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 29 Oct 2024 18:46:34 +0100 Subject: [PATCH 09/77] minor refactoring --- .../actors/ruleChain/DefaultTbContext.java | 56 ++++++++----------- .../actors/rule/DefaultTbContextTest.java | 19 ++++--- .../common/data/rule/DebugStrategy.java | 4 -- 3 files changed, 34 insertions(+), 45 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 3be7524c85..bd57f7da38 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -155,12 +155,7 @@ public class DefaultTbContext implements TbContext { @Override public void tellNext(TbMsg msg, Set relationTypes) { RuleNode ruleNode = nodeCtx.getSelf(); - DebugStrategy debugStrategy = ruleNode.getDebugStrategy(); - if (debugStrategy.shouldPersistDebugOutputForAllEvents(ruleNode.getLastUpdateTs(), msg.getTs(), getMaxRuleNodeDebugDurationMinutes())) { - relationTypes.forEach(relationType -> mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), msg, relationType)); - } else if (debugStrategy.shouldPersistDebugForFailureEventOnly(relationTypes)) { - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), msg, TbNodeConnectionType.FAILURE); - } + persistDebugOutput(msg, relationTypes); msg.getCallback().onProcessingEnd(ruleNode.getId()); nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(ruleNode.getRuleChainId(), ruleNode.getId(), relationTypes, msg, null)); } @@ -183,13 +178,7 @@ public class DefaultTbContext implements TbContext { if (item == null) { ack(msg); } else { - RuleNode ruleNode = nodeCtx.getSelf(); - DebugStrategy debugStrategy = ruleNode.getDebugStrategy(); - if (debugStrategy.shouldPersistDebugOutputForAllEvents(ruleNode.getLastUpdateTs(), msg.getTs(), getMaxRuleNodeDebugDurationMinutes())) { - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), msg, relationType); - } else if (debugStrategy.shouldPersistDebugForFailureEventOnly(relationType)) { - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), msg, relationType); - } + persistDebugOutput(msg, relationType); nodeCtx.getChainActor().tell(new RuleChainOutputMsg(item.getRuleChainId(), item.getRuleNodeId(), relationType, msg)); } } @@ -220,11 +209,7 @@ public class DefaultTbContext implements TbContext { .setTbMsg(TbMsg.toByteString(tbMsg)).build(); mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg, new SimpleTbQueueCallback( metadata -> { - RuleNode ruleNode = nodeCtx.getSelf(); - DebugStrategy debugStrategy = ruleNode.getDebugStrategy(); - if (debugStrategy.shouldPersistDebugOutputForAllEvents(ruleNode.getLastUpdateTs(), tbMsg.getTs(), getMaxRuleNodeDebugDurationMinutes())) { - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), tbMsg, TbNodeConnectionType.TO_ROOT_RULE_CHAIN); - } + persistDebugOutput(tbMsg, TbNodeConnectionType.TO_ROOT_RULE_CHAIN); if (onSuccess != null) { onSuccess.run(); } @@ -320,13 +305,7 @@ public class DefaultTbContext implements TbContext { } mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg.build(), new SimpleTbQueueCallback( metadata -> { - DebugStrategy debugStrategy = ruleNode.getDebugStrategy(); - if (debugStrategy.shouldPersistDebugOutputForAllEvents(ruleNode.getLastUpdateTs(), tbMsg.getTs(), getMaxRuleNodeDebugDurationMinutes())) { - relationTypes.forEach(relationType -> - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), tbMsg, relationType, null, failureMessage)); - } else if (debugStrategy.shouldPersistDebugForFailureEventOnly(relationTypes)) { - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), tbMsg, TbNodeConnectionType.FAILURE, null, failureMessage); - } + persistDebugOutput(tbMsg, relationTypes, null, failureMessage); if (onSuccess != null) { onSuccess.run(); } @@ -343,10 +322,7 @@ public class DefaultTbContext implements TbContext { @Override public void ack(TbMsg tbMsg) { RuleNode ruleNode = nodeCtx.getSelf(); - DebugStrategy debugStrategy = ruleNode.getDebugStrategy(); - if (debugStrategy.shouldPersistDebugOutputForAllEvents(ruleNode.getLastUpdateTs(), tbMsg.getTs(), getMaxRuleNodeDebugDurationMinutes())) { - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), tbMsg, TbNodeConnectionType.ACK); - } + persistDebugOutput(tbMsg, TbNodeConnectionType.ACK); tbMsg.getCallback().onProcessingEnd(ruleNode.getId()); tbMsg.getCallback().onSuccess(); } @@ -363,9 +339,7 @@ public class DefaultTbContext implements TbContext { @Override public void tellFailure(TbMsg msg, Throwable th) { RuleNode ruleNode = nodeCtx.getSelf(); - if (ruleNode.getDebugStrategy().shouldPersistDebugForFailureEventOnly(ruleNode.getLastUpdateTs(), msg.getTs(), getMaxRuleNodeDebugDurationMinutes())) { - mainCtx.persistDebugOutput(nodeCtx.getTenantId(), ruleNode.getId(), msg, TbNodeConnectionType.FAILURE, th); - } + persistDebugOutput(msg, Set.of(TbNodeConnectionType.FAILURE), th, null); String failureMessage = getFailureMessage(th); nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(ruleNode.getRuleChainId(), ruleNode.getId(), Collections.singleton(TbNodeConnectionType.FAILURE), @@ -1018,6 +992,24 @@ public class DefaultTbContext implements TbContext { return failureMessage; } + private void persistDebugOutput(TbMsg msg, String relationType) { + persistDebugOutput(msg, Set.of(relationType)); + } + + private void persistDebugOutput(TbMsg msg, Set relationTypes) { + persistDebugOutput(msg, relationTypes, null, null); + } + + private void persistDebugOutput(TbMsg msg, Set relationTypes, Throwable error, String failureMessage) { + RuleNode ruleNode = nodeCtx.getSelf(); + DebugStrategy debugStrategy = ruleNode.getDebugStrategy(); + if (debugStrategy.shouldPersistDebugOutputForAllEvents(ruleNode.getLastUpdateTs(), msg.getTs(), getMaxRuleNodeDebugDurationMinutes())) { + relationTypes.forEach(relationType -> mainCtx.persistDebugOutput(getTenantId(), ruleNode.getId(), msg, relationType, error, failureMessage)); + } else if (debugStrategy.shouldPersistDebugForFailureEventOnly(relationTypes)) { + mainCtx.persistDebugOutput(getTenantId(), ruleNode.getId(), msg, TbNodeConnectionType.FAILURE, error, failureMessage); + } + } + private int getMaxRuleNodeDebugDurationMinutes() { if (!DebugStrategy.ALL_EVENTS.equals(nodeCtx.getSelf().getDebugStrategy())) { return 0; diff --git a/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java b/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java index e9b344866f..f9ddf7d3fd 100644 --- a/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java +++ b/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java @@ -69,6 +69,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; @@ -161,7 +162,7 @@ class DefaultTbContextTest { // THEN then(nodeCtxMock).should().getChainActor(); then(nodeCtxMock).shouldHaveNoMoreInteractions(); - then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, TbNodeConnectionType.FAILURE); + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, TbNodeConnectionType.FAILURE, null, null); then(mainCtxMock).shouldHaveNoMoreInteractions(); checkTellNextCommonLogic(callbackMock, connections, msg); } @@ -226,7 +227,7 @@ class DefaultTbContextTest { then(nodeCtxMock).shouldHaveNoMoreInteractions(); then(mainCtxMock).should().getTenantProfileCache(); then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); - then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, connection); + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, connection, null, null); then(mainCtxMock).shouldHaveNoMoreInteractions(); checkTellNextCommonLogic(callbackMock, connection, msg); } @@ -260,7 +261,7 @@ class DefaultTbContextTest { then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); var nodeConnectionsCaptor = ArgumentCaptor.forClass(String.class); int wantedNumberOfInvocations = connections.size(); - then(mainCtxMock).should(times(wantedNumberOfInvocations)).persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), eq(msg), nodeConnectionsCaptor.capture()); + then(mainCtxMock).should(times(wantedNumberOfInvocations)).persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), eq(msg), nodeConnectionsCaptor.capture(), nullable(Throwable.class), nullable(String.class)); then(mainCtxMock).shouldHaveNoMoreInteractions(); assertThat(nodeConnectionsCaptor.getAllValues()).hasSize(wantedNumberOfInvocations); assertThat(nodeConnectionsCaptor.getAllValues()).containsExactlyInAnyOrderElementsOf(connections); @@ -288,7 +289,7 @@ class DefaultTbContextTest { // THEN checkOutputCommonLogic(msgMock, TbNodeConnectionType.FAILURE); - then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, TbNodeConnectionType.FAILURE); + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, TbNodeConnectionType.FAILURE, null, null); then(mainCtxMock).shouldHaveNoMoreInteractions(); then(nodeCtxMock).shouldHaveNoMoreInteractions(); } @@ -355,7 +356,7 @@ class DefaultTbContextTest { checkOutputCommonLogic(msgMock, nodeConnection); then(mainCtxMock).should().getTenantProfileCache(); then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); - then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, nodeConnection); + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, nodeConnection, null, null); then(mainCtxMock).shouldHaveNoMoreInteractions(); then(nodeCtxMock).shouldHaveNoMoreInteractions(); } @@ -405,7 +406,7 @@ class DefaultTbContextTest { then(callbackMock).should().onProcessingEnd(RULE_NODE_ID); then(callbackMock).should().onSuccess(); then(nodeCtxMock).should(never()).getChainActor(); - then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, TbNodeConnectionType.ACK); + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, TbNodeConnectionType.ACK, null, null); } @Test @@ -644,7 +645,7 @@ class DefaultTbContextTest { if (DebugStrategy.ALL_EVENTS.equals(debugStrategy)) { then(mainCtxMock).should().getTenantProfileCache(); then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); - then(mainCtxMock).should().persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), eq(msg), eq(TbNodeConnectionType.TO_ROOT_RULE_CHAIN)); + then(mainCtxMock).should().persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), eq(msg), eq(TbNodeConnectionType.TO_ROOT_RULE_CHAIN), nullable(Throwable.class), nullable(String.class)); } then(mainCtxMock).should().getClusterService(); then(mainCtxMock).shouldHaveNoMoreInteractions(); @@ -676,7 +677,7 @@ class DefaultTbContextTest { then(chainActorMock).should().tell(expectedRuleNodeToRuleChainTellNextMsg); then(chainActorMock).shouldHaveNoMoreInteractions(); then(nodeCtxMock).should().getChainActor(); - then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, TbNodeConnectionType.FAILURE, EXCEPTION); + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, TbNodeConnectionType.FAILURE, EXCEPTION, null); then(mainCtxMock).shouldHaveNoMoreInteractions(); then(nodeCtxMock).shouldHaveNoMoreInteractions(); } @@ -736,7 +737,7 @@ class DefaultTbContextTest { then(chainActorMock).should().tell(expectedRuleNodeToRuleChainTellNextMsg); then(chainActorMock).shouldHaveNoMoreInteractions(); then(nodeCtxMock).should().getChainActor(); - then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, TbNodeConnectionType.FAILURE, EXCEPTION); + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, TbNodeConnectionType.FAILURE, EXCEPTION, null); then(mainCtxMock).should().getTenantProfileCache(); then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); then(mainCtxMock).shouldHaveNoMoreInteractions(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/DebugStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DebugStrategy.java index 333ee9c670..e23b1bea94 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/DebugStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DebugStrategy.java @@ -39,10 +39,6 @@ public enum DebugStrategy { return isAllEventsStrategyAndMsgTsWithinDebugDuration(lastUpdateTs, msgTs, debugModeDurationMinutes); } - public boolean shouldPersistDebugForFailureEventOnly(long lastUpdateTs, long msgTs, int debugModeDurationMinutes) { - return shouldPersistDebugOutputForAllEvents(lastUpdateTs, msgTs, debugModeDurationMinutes) || isFailureOnlyStrategy(); - } - public boolean shouldPersistDebugForFailureEventOnly(Set nodeConnections) { return isFailureOnlyStrategy() && nodeConnections.contains(TbNodeConnectionType.FAILURE); } From c676f79fc32d5ab32a9a202c0d059d3da9114968 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 4 Nov 2024 20:23:35 +0100 Subject: [PATCH 10/77] added maxRuleNodeDebugDurationMinutes to the SystemParams --- .../thingsboard/server/controller/SystemInfoController.java | 5 +++++ .../org/thingsboard/server/common/data/SystemParams.java | 1 + ui-ngx/src/app/core/auth/auth.models.ts | 1 + 3 files changed, 7 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 9600ac0221..2c15aeec95 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.annotations.Hidden; import jakarta.annotation.PostConstruct; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -70,6 +71,9 @@ public class SystemInfoController extends BaseController { @Value("${ui.dashboard.max_datapoints_limit}") private long maxDatapointsLimit; + @Value("${actors.rule.node.max_debug_mode_duration:60}") + private int maxRuleNodeDebugModeDurationMinutes; + @Autowired(required = false) private BuildProperties buildProperties; @@ -141,6 +145,7 @@ public class SystemInfoController extends BaseController { if (!currentUser.isSystemAdmin()) { DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration(); systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize()); + systemParams.setMaxRuleNodeDebugDurationMinutes(tenantProfileConfiguration.getMaxRuleNodeDebugModeDurationMinutes(maxRuleNodeDebugModeDurationMinutes)); } systemParams.setMobileQrEnabled(Optional.ofNullable(mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID)) .map(MobileAppSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index 5b4312f7b0..a3c9c0d51b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -32,4 +32,5 @@ public class SystemParams { long maxDatapointsLimit; long maxResourceSize; boolean mobileQrEnabled; + int maxRuleNodeDebugDurationMinutes; } diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index b73847ef83..cfc3fed4ba 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -27,6 +27,7 @@ export interface SysParamsState { mobileQrEnabled: boolean; userSettings: UserSettings; maxResourceSize: number; + maxRuleNodeDebugDurationMinutes: number; } export interface SysParams extends SysParamsState { From 2e28696068d289ec994f607db87257d6d29cbb04 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 5 Nov 2024 10:26:08 +0100 Subject: [PATCH 11/77] reverted changes on UI --- ui-ngx/src/app/core/auth/auth.models.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index cfc3fed4ba..b73847ef83 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -27,7 +27,6 @@ export interface SysParamsState { mobileQrEnabled: boolean; userSettings: UserSettings; maxResourceSize: number; - maxRuleNodeDebugDurationMinutes: number; } export interface SysParams extends SysParamsState { From 5c6cd2dda89bc2d4b3b2f83a9bbff6f4f26bfae8 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 5 Nov 2024 20:18:15 +0100 Subject: [PATCH 12/77] added new debug strategy ALL_THEN_ONLY_FAILURE_EVENTS --- .../actors/ruleChain/DefaultTbContext.java | 14 +- .../RuleChainActorMessageProcessor.java | 2 +- .../actors/rule/DefaultTbContextTest.java | 192 +++++++++++++++++- .../common/data/rule/DebugStrategy.java | 28 +-- common/edge-api/src/main/proto/edge.proto | 3 +- 5 files changed, 212 insertions(+), 27 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index bd57f7da38..119d1d2900 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -1005,19 +1005,19 @@ public class DefaultTbContext implements TbContext { DebugStrategy debugStrategy = ruleNode.getDebugStrategy(); if (debugStrategy.shouldPersistDebugOutputForAllEvents(ruleNode.getLastUpdateTs(), msg.getTs(), getMaxRuleNodeDebugDurationMinutes())) { relationTypes.forEach(relationType -> mainCtx.persistDebugOutput(getTenantId(), ruleNode.getId(), msg, relationType, error, failureMessage)); - } else if (debugStrategy.shouldPersistDebugForFailureEventOnly(relationTypes)) { + } else if (debugStrategy.shouldPersistDebugForFailureEvent(relationTypes)) { mainCtx.persistDebugOutput(getTenantId(), ruleNode.getId(), msg, TbNodeConnectionType.FAILURE, error, failureMessage); } } private int getMaxRuleNodeDebugDurationMinutes() { - if (!DebugStrategy.ALL_EVENTS.equals(nodeCtx.getSelf().getDebugStrategy())) { - return 0; + if (nodeCtx.getSelf().getDebugStrategy().isHasDuration()) { + var configuration = mainCtx.getTenantProfileCache() + .get(getTenantId()).getProfileData().getConfiguration(); + int systemMaxRuleNodeDebugModeDurationMinutes = mainCtx.getMaxRuleNodeDebugModeDurationMinutes(); + return configuration.getMaxRuleNodeDebugModeDurationMinutes(systemMaxRuleNodeDebugModeDurationMinutes); } - var configuration = mainCtx.getTenantProfileCache() - .get(getTenantId()).getProfileData().getConfiguration(); - int systemMaxRuleNodeDebugModeDurationMinutes = mainCtx.getMaxRuleNodeDebugModeDurationMinutes(); - return configuration.getMaxRuleNodeDebugModeDurationMinutes(systemMaxRuleNodeDebugModeDurationMinutes); + return 0; } } diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java index 2213daa770..01caa50860 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java @@ -260,7 +260,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor givenDebugStrategyAllThenOnlyFailureEventsAndConnection_whenTellNext_thenVerifyDebugOutputPersisted() { + return failureAndSuccessConnection(); + } + + @Test + public void givenDebugStrategyAllThenOnlyEventsAndFailureAndSuccessConnection_whenTellNext_thenVerifyDebugOutputPersistedForAllEvents() { + // GIVEN + var callbackMock = mock(TbMsgCallback.class); + var msg = getTbMsgWithCallback(callbackMock); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setLastUpdateTs(System.currentTimeMillis()); + ruleNode.setDebugStrategy(DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); + mockGetMaxRuleNodeDebugModeDurationMinutes(); + + // WHEN + Set connections = failureAndSuccessConnection().collect(Collectors.toSet()); + defaultTbContext.tellNext(msg, connections); + + // THEN + then(nodeCtxMock).should().getChainActor(); + then(nodeCtxMock).shouldHaveNoMoreInteractions(); + then(mainCtxMock).should().getTenantProfileCache(); + then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); + var nodeConnectionsCaptor = ArgumentCaptor.forClass(String.class); + int wantedNumberOfInvocations = connections.size(); + then(mainCtxMock).should(times(wantedNumberOfInvocations)).persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), eq(msg), nodeConnectionsCaptor.capture(), nullable(Throwable.class), nullable(String.class)); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + assertThat(nodeConnectionsCaptor.getAllValues()).hasSize(wantedNumberOfInvocations); + assertThat(nodeConnectionsCaptor.getAllValues()).containsExactlyInAnyOrderElementsOf(connections); + checkTellNextCommonLogic(callbackMock, connections, msg); + } + private static Stream failureAndSuccessConnection() { return Stream.of(TbNodeConnectionType.FAILURE, TbNodeConnectionType.SUCCESS); } @@ -361,6 +427,32 @@ class DefaultTbContextTest { then(nodeCtxMock).shouldHaveNoMoreInteractions(); } + @ParameterizedTest + @ValueSource(strings = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE}) + void givenDebugStrategyAllThenOnlyFailureEvents_whenOutput_thenVerifyDebugOutputPersisted(String nodeConnection) { + // GIVEN + var msgMock = mock(TbMsg.class); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.ALL_EVENTS); + given(msgMock.popFormStack()).willReturn(new TbMsgProcessingStackItem(RULE_CHAIN_ID, RULE_NODE_ID)); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); + mockGetMaxRuleNodeDebugModeDurationMinutes(); + + // WHEN + defaultTbContext.output(msgMock, nodeConnection); + + // THEN + checkOutputCommonLogic(msgMock, nodeConnection); + then(mainCtxMock).should().getTenantProfileCache(); + then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, nodeConnection, null, null); + then(mainCtxMock).shouldHaveNoMoreInteractions(); + then(nodeCtxMock).shouldHaveNoMoreInteractions(); + } + @Test public void givenEmptyStack_whenOutput_thenVerifyMsgAck() { // GIVEN @@ -409,6 +501,32 @@ class DefaultTbContextTest { then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, TbNodeConnectionType.ACK, null, null); } + @Test + public void givenEmptyStackAndDebugStrategyAllThenOnlyFailureEvents_whenOutput_thenVerifyMsgAckAndDebugOutputPersisted() { + // GIVEN + var msgMock = mock(TbMsg.class); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setDebugStrategy(DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS); + ruleNode.setLastUpdateTs(System.currentTimeMillis()); + given(msgMock.popFormStack()).willReturn(null); + TbMsgCallback callbackMock = mock(TbMsgCallback.class); + given(msgMock.getCallback()).willReturn(callbackMock); + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + mockGetMaxRuleNodeDebugModeDurationMinutes(); + + // WHEN + defaultTbContext.output(msgMock, TbNodeConnectionType.SUCCESS); + + // THEN + then(msgMock).should().popFormStack(); + then(callbackMock).should().onProcessingEnd(RULE_NODE_ID); + then(callbackMock).should().onSuccess(); + then(nodeCtxMock).should(never()).getChainActor(); + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, TbNodeConnectionType.ACK, null, null); + } + @Test public void givenDebugStrategyOnlyFailureEvents_whenEnqueueForTellFailure_thenVerifyDebugOutputPersisted() { // GIVEN @@ -551,7 +669,7 @@ class DefaultTbContextTest { given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi); given(mainCtxMock.getClusterService()).willReturn(tbClusterServiceMock); - if (DebugStrategy.ALL_EVENTS.equals(debugStrategy)) { + if (DebugStrategy.ALL_EVENTS.equals(debugStrategy) || DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS.equals(debugStrategy)) { mockGetMaxRuleNodeDebugModeDurationMinutes(); } @@ -579,7 +697,7 @@ class DefaultTbContextTest { assertThat(simpleTbQueueCallback).isNotNull(); simpleTbQueueCallback.onSuccess(null); - if (DebugStrategy.ALL_EVENTS.equals(debugStrategy)) { + if (DebugStrategy.ALL_EVENTS.equals(debugStrategy) || DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS.equals(debugStrategy)) { then(mainCtxMock).should().getTenantProfileCache(); then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); ArgumentCaptor tbMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); @@ -611,7 +729,7 @@ class DefaultTbContextTest { given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi); given(mainCtxMock.getClusterService()).willReturn(tbClusterServiceMock); - if (DebugStrategy.ALL_EVENTS.equals(debugStrategy)) { + if (DebugStrategy.ALL_EVENTS.equals(debugStrategy) || DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS.equals(debugStrategy)) { mockGetMaxRuleNodeDebugModeDurationMinutes(); } @@ -642,7 +760,7 @@ class DefaultTbContextTest { assertThat(simpleTbQueueCallback).isNotNull(); simpleTbQueueCallback.onSuccess(null); - if (DebugStrategy.ALL_EVENTS.equals(debugStrategy)) { + if (debugStrategy.isHasDuration()) { then(mainCtxMock).should().getTenantProfileCache(); then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); then(mainCtxMock).should().persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), eq(msg), eq(TbNodeConnectionType.TO_ROOT_RULE_CHAIN), nullable(Throwable.class), nullable(String.class)); @@ -744,6 +862,51 @@ class DefaultTbContextTest { then(nodeCtxMock).shouldHaveNoMoreInteractions(); } + @MethodSource + @ParameterizedTest + void givenDebugStrategyAndConnectionAndPersistedResultOptions_whenTellNext_thenVerifyDebugOutputPersistence(DebugStrategy debugStrategy, + String connection, + boolean shouldPersist, + boolean shouldPersistAfterDurationTime) { + // GIVEN + var callbackMock = mock(TbMsgCallback.class); + var msg = getTbMsgWithCallback(callbackMock); + var ruleNode = new RuleNode(RULE_NODE_ID); + ruleNode.setRuleChainId(RULE_CHAIN_ID); + ruleNode.setLastUpdateTs(System.currentTimeMillis()); + ruleNode.setDebugStrategy(debugStrategy); + if (shouldPersist) { + given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); + } + given(nodeCtxMock.getSelf()).willReturn(ruleNode); + given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); + if (debugStrategy.isHasDuration()) { + mockGetMaxRuleNodeDebugModeDurationMinutes(); + } + + // WHEN + defaultTbContext.tellNext(msg, connection); + + // THEN + if (shouldPersist) { + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, connection, null, null); + } + + // GIVEN + Mockito.clearInvocations(mainCtxMock); + if (debugStrategy.isHasDuration()) { + mockGetMaxRuleNodeDebugModeDurationMinutes(0); + } + + // WHEN + defaultTbContext.tellNext(msg, connection); + + // THEN + if (shouldPersistAfterDurationTime) { + then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, connection, null, null); + } + } + private void checkTellNextCommonLogic(TbMsgCallback callbackMock, String nodeConnection, TbMsg msg) { checkTellNextCommonLogic(callbackMock, Collections.singleton(nodeConnection), msg); } @@ -796,6 +959,7 @@ class DefaultTbContextTest { private static Stream givenDebugStrategyOptions_whenEnqueueForTellNext_thenVerifyDebugOutputPersistedOnlyForAllEventsDebugStrategy() { return Stream.of( Arguments.of(DebugStrategy.ALL_EVENTS, TbNodeConnectionType.OTHER), + Arguments.of(DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS, TbNodeConnectionType.OTHER), Arguments.of(DebugStrategy.ONLY_FAILURE_EVENTS, TbNodeConnectionType.TRUE), Arguments.of(DebugStrategy.DISABLED, TbNodeConnectionType.FALSE) ); @@ -804,11 +968,25 @@ class DefaultTbContextTest { private static Stream givenDebugStrategyOptions_whenEnqueue_thenVerifyDebugOutputPersistedOnlyForAllEventsDebugStrategy() { return Stream.of( Arguments.of(DebugStrategy.ALL_EVENTS), + Arguments.of(DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS), Arguments.of(DebugStrategy.ONLY_FAILURE_EVENTS), Arguments.of(DebugStrategy.DISABLED) ); } + private static Stream givenDebugStrategyAndConnectionAndPersistedResultOptions_whenTellNext_thenVerifyDebugOutputPersistence() { + return Stream.of( + Arguments.of(DebugStrategy.ALL_EVENTS, TbNodeConnectionType.SUCCESS, true, false), + Arguments.of(DebugStrategy.ALL_EVENTS, TbNodeConnectionType.FAILURE, true, false), + Arguments.of(DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS, TbNodeConnectionType.SUCCESS, true, false), + Arguments.of(DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS, TbNodeConnectionType.FAILURE, true, true), + Arguments.of(DebugStrategy.ONLY_FAILURE_EVENTS, TbNodeConnectionType.SUCCESS, false, false), + Arguments.of(DebugStrategy.ONLY_FAILURE_EVENTS, TbNodeConnectionType.FAILURE, true, true), + Arguments.of(DebugStrategy.DISABLED, TbNodeConnectionType.SUCCESS, false, false), + Arguments.of(DebugStrategy.DISABLED, TbNodeConnectionType.FAILURE, false, false) + ); + } + private TbMsg getTbMsgWithCallback(TbMsgCallback callback) { return TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TENANT_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING, callback); } @@ -822,6 +1000,10 @@ class DefaultTbContextTest { } private void mockGetMaxRuleNodeDebugModeDurationMinutes() { + mockGetMaxRuleNodeDebugModeDurationMinutes(15); + } + + private void mockGetMaxRuleNodeDebugModeDurationMinutes(int maxRuleNodeDebugModeDurationMinutes) { var tbTenantProfileCacheMock = mock(TbTenantProfileCache.class); var tenantProfileMock = mock(TenantProfile.class); var tenantProfileDataMock = mock(TenantProfileData.class); @@ -831,7 +1013,7 @@ class DefaultTbContextTest { given(tbTenantProfileCacheMock.get(TENANT_ID)).willReturn(tenantProfileMock); given(tenantProfileMock.getProfileData()).willReturn(tenantProfileDataMock); given(tenantProfileDataMock.getConfiguration()).willReturn(tenantProfileConfigurationMock); - given(tenantProfileConfigurationMock.getMaxRuleNodeDebugModeDurationMinutes(anyInt())).willReturn(15); + given(tenantProfileConfigurationMock.getMaxRuleNodeDebugModeDurationMinutes(anyInt())).willReturn(maxRuleNodeDebugModeDurationMinutes); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/DebugStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DebugStrategy.java index e23b1bea94..a065c9065f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/DebugStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DebugStrategy.java @@ -23,12 +23,17 @@ import java.util.concurrent.TimeUnit; @Getter public enum DebugStrategy { - DISABLED(0), ALL_EVENTS(1), ONLY_FAILURE_EVENTS(2); + DISABLED(0, false), + ALL_EVENTS(1, true), + ALL_THEN_ONLY_FAILURE_EVENTS(2, true), + ONLY_FAILURE_EVENTS(3, false); private final int protoNumber; + private final boolean hasDuration; - DebugStrategy(int protoNumber) { + DebugStrategy(int protoNumber, boolean hasDuration) { this.protoNumber = protoNumber; + this.hasDuration = hasDuration; } public boolean shouldPersistDebugInput(long lastUpdateTs, long msgTs, int debugModeDurationMinutes) { @@ -36,26 +41,23 @@ public enum DebugStrategy { } public boolean shouldPersistDebugOutputForAllEvents(long lastUpdateTs, long msgTs, int debugModeDurationMinutes) { - return isAllEventsStrategyAndMsgTsWithinDebugDuration(lastUpdateTs, msgTs, debugModeDurationMinutes); + return this.isAllEventsStrategyAndMsgTsWithinDebugDuration(lastUpdateTs, msgTs, debugModeDurationMinutes); } - public boolean shouldPersistDebugForFailureEventOnly(Set nodeConnections) { - return isFailureOnlyStrategy() && nodeConnections.contains(TbNodeConnectionType.FAILURE); + public boolean shouldPersistDebugForFailureEvent(Set nodeConnections) { + return isFailureStrategy() && nodeConnections.contains(TbNodeConnectionType.FAILURE); } - public boolean shouldPersistDebugForFailureEventOnly(String nodeConnection) { - return isFailureOnlyStrategy() && TbNodeConnectionType.FAILURE.equals(nodeConnection); + public boolean shouldPersistDebugForFailureEvent(String nodeConnection) { + return isFailureStrategy() && TbNodeConnectionType.FAILURE.equals(nodeConnection); } - private boolean isFailureOnlyStrategy() { - return DebugStrategy.ONLY_FAILURE_EVENTS.equals(this); + private boolean isFailureStrategy() { + return DebugStrategy.ONLY_FAILURE_EVENTS.equals(this) || DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS.equals(this); } private boolean isAllEventsStrategyAndMsgTsWithinDebugDuration(long lastUpdateTs, long msgTs, int debugModeDurationMinutes) { - if (!DebugStrategy.ALL_EVENTS.equals(this)) { - return false; - } - return isMsgTsWithinDebugDuration(lastUpdateTs, msgTs, debugModeDurationMinutes); + return this.hasDuration && isMsgTsWithinDebugDuration(lastUpdateTs, msgTs, debugModeDurationMinutes); } private boolean isMsgTsWithinDebugDuration(long lastUpdateTs, long msgCreationTs, int debugModeDurationMinutes) { diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index f5765f3103..167e160648 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -165,7 +165,8 @@ message RuleChainMetadataUpdateMsg { enum DebugStrategy { DISABLED = 0; ALL_EVENTS = 1; - ONLY_FAILURE_EVENTS = 2; + ALL_THEN_ONLY_FAILURE_EVENTS = 2; + ONLY_FAILURE_EVENTS = 3; } message RuleNodeProto { From 9dbade0b78043ed9588590c42285e6194579adf2 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 5 Nov 2024 20:24:28 +0100 Subject: [PATCH 13/77] added ruleChainDebugPerTenantLimitsConfiguration to the system params --- .../server/controller/SystemInfoController.java | 11 +++++++++++ .../thingsboard/server/common/data/SystemParams.java | 1 + 2 files changed, 12 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 2c15aeec95..7c73a26a90 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -74,6 +74,14 @@ public class SystemInfoController extends BaseController { @Value("${actors.rule.node.max_debug_mode_duration:60}") private int maxRuleNodeDebugModeDurationMinutes; + @Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.enabled:true}") + @Getter + private boolean ruleChainDebugPerTenantLimitsEnabled; + + @Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.configuration:50000:3600}") + @Getter + private String ruleChainDebugPerTenantLimitsConfiguration; + @Autowired(required = false) private BuildProperties buildProperties; @@ -146,6 +154,9 @@ public class SystemInfoController extends BaseController { DefaultTenantProfileConfiguration tenantProfileConfiguration = tenantProfileCache.get(tenantId).getDefaultProfileConfiguration(); systemParams.setMaxResourceSize(tenantProfileConfiguration.getMaxResourceSize()); systemParams.setMaxRuleNodeDebugDurationMinutes(tenantProfileConfiguration.getMaxRuleNodeDebugModeDurationMinutes(maxRuleNodeDebugModeDurationMinutes)); + if (ruleChainDebugPerTenantLimitsEnabled) { + systemParams.setRuleChainDebugPerTenantLimitsConfiguration(ruleChainDebugPerTenantLimitsConfiguration); + } } systemParams.setMobileQrEnabled(Optional.ofNullable(mobileAppSettingsService.getMobileAppSettings(TenantId.SYS_TENANT_ID)) .map(MobileAppSettings::getQrCodeConfig).map(QRCodeConfig::isShowOnHomePage) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index a3c9c0d51b..0f84567800 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -33,4 +33,5 @@ public class SystemParams { long maxResourceSize; boolean mobileQrEnabled; int maxRuleNodeDebugDurationMinutes; + String ruleChainDebugPerTenantLimitsConfiguration; } From 3c7dc0cc0396385ff2bf6f34c7a67deb327adacd Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 6 Nov 2024 19:53:06 +0200 Subject: [PATCH 14/77] UI implementation of Rule Node debug strategy --- ui-ngx/src/app/core/auth/auth.models.ts | 1 + ui-ngx/src/app/core/auth/auth.reducer.ts | 3 +- ...enant-profile-configuration.component.html | 16 +++ ...-tenant-profile-configuration.component.ts | 1 + .../rulechain/debug-duration-left.pipe.ts | 37 ++++++ .../debug-strategy-button.component.html | 32 +++++ .../debug-strategy-button.component.ts | 120 ++++++++++++++++++ .../debug-strategy-panel.component.html | 50 ++++++++ .../debug-strategy-panel.component.ts | 94 ++++++++++++++ .../rule-node-details.component.html | 22 ++-- .../rule-node-details.component.scss | 7 - .../rulechain/rule-node-details.component.ts | 11 +- .../rulechain/rulechain-page.component.ts | 14 +- .../home/pages/rulechain/rulechain.module.ts | 6 +- .../import-export/import-export.service.ts | 4 +- .../src/app/shared/models/rule-node.models.ts | 13 +- .../assets/locale/locale.constant-ar_AE.json | 1 - .../assets/locale/locale.constant-en_US.json | 19 ++- .../assets/locale/locale.constant-es_ES.json | 1 - .../assets/locale/locale.constant-lt_LT.json | 1 - .../assets/locale/locale.constant-nl_BE.json | 1 - .../assets/locale/locale.constant-pl_PL.json | 1 - .../assets/locale/locale.constant-zh_CN.json | 1 - ui-ngx/src/styles.scss | 17 +++ 24 files changed, 438 insertions(+), 35 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.ts diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index b73847ef83..cfc3fed4ba 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -27,6 +27,7 @@ export interface SysParamsState { mobileQrEnabled: boolean; userSettings: UserSettings; maxResourceSize: number; + maxRuleNodeDebugDurationMinutes: number; } export interface SysParams extends SysParamsState { diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index 73e0d04eb1..e2ff1fae25 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -31,7 +31,8 @@ const emptyUserAuthState: AuthPayload = { persistDeviceStateToTelemetry: false, mobileQrEnabled: false, maxResourceSize: 0, - userSettings: initialUserSettings + userSettings: initialUserSettings, + maxRuleNodeDebugDurationMinutes: 0, }; export const initialState: AuthState = { diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index d511125eec..2e5d698121 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -147,6 +147,22 @@ +
+ + tenant-profile.maximum-rule-node-debug-duration-min + + + {{ 'tenant-profile.maximum-rule-node-debug-duration-min-range' | translate}} + + + {{ 'tenant-profile.maximum-rule-node-debug-duration-min-required' | translate}} + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts index f7261e7957..860328fb80 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts @@ -85,6 +85,7 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA tenantNotificationRequestsRateLimit: [null, []], tenantNotificationRequestsPerRuleRateLimit: [null, []], maxTransportMessages: [null, [Validators.required, Validators.min(0)]], + maxRuleNodeDebugDurationMinutes: [null, [Validators.required, Validators.min(0)]], maxTransportDataPoints: [null, [Validators.required, Validators.min(0)]], maxREExecutions: [null, [Validators.required, Validators.min(0)]], maxJSExecutions: [null, [Validators.required, Validators.min(0)]], diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts b/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts new file mode 100644 index 0000000000..17176bd4af --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts @@ -0,0 +1,37 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Pipe, PipeTransform } from '@angular/core'; +import { MINUTE } from '@shared/models/time/time.models'; + +@Pipe({ + name: 'debugDurationLeft', + pure: false, + standalone: true, +}) +export class DebugDurationLeftPipe implements PipeTransform { + transform(lastUpdateTs: number, maxRuleNodeDebugDurationMinutes: number): string { + const minutes = Math.max(0, Math.floor(this.getDebugTime(lastUpdateTs, maxRuleNodeDebugDurationMinutes) / MINUTE)); + return `${minutes} min left`; + } + + getDebugTime(lastUpdateTs: number, maxRuleNodeDebugDurationMinutes: number): number { + const maxDuration = maxRuleNodeDebugDurationMinutes * MINUTE; + const updateDuration = lastUpdateTs ? new Date().getTime() - lastUpdateTs : 0; + const leftTime = maxDuration - updateDuration; + return leftTime > 0 ? leftTime : 0; + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.html new file mode 100644 index 0000000000..800f54ae3e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.html @@ -0,0 +1,32 @@ + + diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.ts new file mode 100644 index 0000000000..9a158d6a85 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.ts @@ -0,0 +1,120 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + Input, + forwardRef, + Renderer2, + ViewContainerRef, + DestroyRef, + ChangeDetectionStrategy, + ChangeDetectorRef +} from '@angular/core'; +import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, UntypedFormBuilder } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { DebugStrategy } from '@shared/models/rule-node.models'; +import { DebugDurationLeftPipe } from '@home/pages/rulechain/debug-duration-left.pipe'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { MatButton } from '@angular/material/button'; +import { DebugStrategyPanelComponent } from '@home/pages/rulechain/debug-strategy-panel.component'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { interval } from 'rxjs'; +import { MINUTE } from '@shared/models/time/time.models'; + +@Component({ + selector: 'tb-debug-strategy-button', + templateUrl: './debug-strategy-button.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DebugStrategyButtonComponent), + multi: true + } + ], + standalone: true, + imports: [ + CommonModule, + SharedModule, + DebugDurationLeftPipe, + ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DebugStrategyButtonComponent implements ControlValueAccessor { + + @Input() disabled = false; + @Input() lastUpdateTs: number; + + debugStrategyFormControl: FormControl; + + private onChange: (debugStrategy: DebugStrategy) => void; + + readonly maxRuleNodeDebugDurationMinutes = getCurrentAuthState(this.store).maxRuleNodeDebugDurationMinutes; + readonly DebugStrategy = DebugStrategy; + + constructor(protected store: Store, + private fb: UntypedFormBuilder, + private popoverService: TbPopoverService, + private renderer: Renderer2, + private viewContainerRef: ViewContainerRef, + private destroyRef: DestroyRef, + private cdr: ChangeDetectorRef + ) { + this.debugStrategyFormControl = this.fb.control(DebugStrategy.DISABLED); + this.debugStrategyFormControl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.onChange(value)); + interval(0.5 * MINUTE) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => this.cdr.markForCheck()); + } + + openDebugStrategyPanel($event: Event, matButton: MatButton): void { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + const debugStrategy = this.debugStrategyFormControl.value; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const debugStrategyPopover = this.popoverService.displayPopover(trigger, this.renderer, + this.viewContainerRef, DebugStrategyPanelComponent, 'bottom', true, null, + { debugStrategy }, + {}, + {}, {}, true); + debugStrategyPopover.tbComponentRef.instance.popover = debugStrategyPopover; + debugStrategyPopover.tbComponentRef.instance.onStrategyApplied.subscribe((strategy: DebugStrategy) => { + this.debugStrategyFormControl.patchValue(strategy); + this.cdr.markForCheck(); + debugStrategyPopover.hide(); + }); + } + } + + registerOnChange(onChange: (debugStrategy: DebugStrategy) => void): void { + this.onChange = onChange; + } + + registerOnTouched(_: () => {}): void {} + + writeValue(value: DebugStrategy): void { + this.debugStrategyFormControl.patchValue(value, { emitEvent: false }); + this.cdr.markForCheck(); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.html new file mode 100644 index 0000000000..4f8402ff3f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.html @@ -0,0 +1,50 @@ + +
+
debug-strategy.label
+
+
{{ 'debug-strategy.hint.main' | translate }}
+
+
+ + + {{ 'debug-strategy.on-failure' | translate }} + + + + + {{ 'debug-strategy.all-messages' | translate }} {{ ' (' + maxRuleNodeDebugDurationMinutes + ' ' }} {{ ('debug-strategy.min' | translate) + ')'}} + + +
+
+ + +
+
+ diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.ts new file mode 100644 index 0000000000..87851067de --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.ts @@ -0,0 +1,94 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { DebugStrategy } from '@shared/models/rule-node.models'; + +@Component({ + selector: 'tb-debug-strategy-panel', + templateUrl: './debug-strategy-panel.component.html', + standalone: true, + imports: [ + SharedModule, + CommonModule + ] +}) +export class DebugStrategyPanelComponent extends PageComponent implements OnInit { + + @Input() popover: TbPopoverComponent; + @Input() debugStrategy: DebugStrategy; + + debugStrategyFormGroup: UntypedFormGroup; + + onStrategyApplied = new EventEmitter() + + readonly maxRuleNodeDebugDurationMinutes = getCurrentAuthState(this.store).maxRuleNodeDebugDurationMinutes; + + constructor(private fb: UntypedFormBuilder, + protected store: Store) { + super(store); + + this.debugStrategyFormGroup = this.fb.group({ + allMessages: [false], + onFailure: [false] + }); + } + + ngOnInit(): void { + this.updatePanelStrategy(); + } + + onCancel() { + this.popover?.hide(); + } + + onApply(): void { + const allMessages = this.debugStrategyFormGroup.get('allMessages').value; + const onFailure = this.debugStrategyFormGroup.get('onFailure').value; + if (allMessages && onFailure) { + this.onStrategyApplied.emit(DebugStrategy.ALL_WITH_FAILURES); + } else if (allMessages) { + this.onStrategyApplied.emit(DebugStrategy.ALL_EVENTS); + } else if (onFailure) { + this.onStrategyApplied.emit(DebugStrategy.ONLY_FAILURE_EVENTS); + } else { + this.onStrategyApplied.emit(DebugStrategy.DISABLED); + } + } + + private updatePanelStrategy(): void { + switch (this.debugStrategy) { + case DebugStrategy.ALL_WITH_FAILURES: + this.debugStrategyFormGroup.get('allMessages').patchValue(true, { emitEvent: false }); + this.debugStrategyFormGroup.get('onFailure').patchValue(true, { emitEvent: false }); + break; + case DebugStrategy.ONLY_FAILURE_EVENTS: + this.debugStrategyFormGroup.get('onFailure').patchValue(true, { emitEvent: false }); + break; + case DebugStrategy.ALL_EVENTS: + this.debugStrategyFormGroup.get('allMessages').patchValue(true, { emitEvent: false }); + break; + } + } +} 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 014a73973c..c64fd2e422 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 @@ -22,7 +22,7 @@
-
+
rulenode.name @@ -34,13 +34,19 @@ {{ 'rulenode.name-max-length' | translate }} -
- - {{ 'rulenode.debug-mode' | translate }} - - - {{ 'rulenode.singleton-mode' | translate }} - +
+ +
{ ruleChainId?: RuleChainId; type: string; name: string; - debugMode: boolean; + debugStrategy: DebugStrategy; + lastUpdateTs?: number; singletonMode: boolean; queueName?: string; configurationVersion?: number; @@ -345,13 +346,21 @@ export interface FcRuleNode extends FcRuleNodeType { ruleNodeId?: RuleNodeId; additionalInfo?: any; configuration?: RuleNodeConfiguration; - debugMode?: boolean; + debugStrategy?: DebugStrategy; + lastUpdateTs?: number; error?: string; highlighted?: boolean; componentClazz?: string; ruleChainType?: RuleChainType; } +export enum DebugStrategy { + DISABLED = 'DISABLED', + ALL_EVENTS = 'ALL_EVENTS', + ONLY_FAILURE_EVENTS = 'ONLY_FAILURE_EVENTS', + ALL_WITH_FAILURES = 'ALL_WITH_FAILURES', +} + export interface FcRuleEdge extends FcEdge { labels?: string[]; } diff --git a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json index 0afd671082..c45a5b4be4 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json +++ b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json @@ -4556,7 +4556,6 @@ "deselect-all": "إلغاء تحديد الكل", "rulenode-details": "تفاصيل عقدة القاعدة", "debug-mode": "وضع التصحيح", - "singleton-mode": "وضع المفرد", "configuration": "التكوين", "link": "رابط", "link-details": "تفاصيل رابط عقدة القاعدة", diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 0fa19cc176..6185257663 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -995,6 +995,19 @@ "type-timeseries-deleted": "Telemetry deleted", "type-sms-sent": "SMS sent" }, + "debug-strategy": { + "min": "min", + "label": "Debug strategy", + "on-failure": "Failures only (24/7)", + "all-messages": "All messages", + "failures": "Failures", + "all": "All", + "hint": { + "main": "All node debug messages rate limited with:", + "on-failure": "Save all failure debug events without time limit.", + "all-messages": "Save all debug events during time limit." + } + }, "confirm-on-exit": { "message": "You have unsaved changes. Are you sure you want to leave this page?", "html-message": "You have unsaved changes.
Are you sure you want to leave this page?", @@ -2071,6 +2084,7 @@ "column": "Column", "row": "Row" }, + "disabled": "Disabled", "edge": { "edge": "Edge", "edge-instances": "Edge instances", @@ -4014,7 +4028,7 @@ "deselect-all": "Deselect all", "rulenode-details": "Rule node details", "debug-mode": "Debug mode", - "singleton-mode": "Singleton mode", + "singleton": "Singleton", "configuration": "Configuration", "link": "Link", "link-details": "Rule node link details", @@ -4292,6 +4306,9 @@ "maximum-ota-packages-sum-data-size": "Maximum total size of OTA package files (bytes)", "maximum-ota-package-sum-data-size-required": "Maximum total size of OTA package files is required.", "maximum-ota-package-sum-data-size-range": "Maximum total size of OTA package files can't be negative", + "maximum-rule-node-debug-duration-min": "Maximum rule node debug duration (min)", + "maximum-rule-node-debug-duration-min-required": "Maximum rule node debug duration is required.", + "maximum-rule-node-debug-duration-min-range": "Maximum rule node debug duration can't be negative", "rest-requests-for-tenant": "REST requests for tenant", "transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages", "transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points", diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index f653e7179a..1ae18bd51c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -3485,7 +3485,6 @@ "deselect-all": "Deshacer selección de todos", "rulenode-details": "Detalles del nodo de reglas", "debug-mode": "Modo Debug", - "singleton-mode": "Módo único", "configuration": "Configuración", "link": "Enlace", "link-details": "Detalles del enlace del nodo de reglas", diff --git a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json index cb3a96f989..4791ab251f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json +++ b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json @@ -4458,7 +4458,6 @@ "deselect-all": "Deselect all", "rulenode-details": "Rule node details", "debug-mode": "Debug mode", - "singleton-mode": "Singleton mode", "configuration": "Configuration", "link": "Link", "link-details": "Rule node link details", diff --git a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json index c16c9c5e40..67d73b8be9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json +++ b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json @@ -4260,7 +4260,6 @@ "deselect-all": "Alles deselecteren", "rulenode-details": "Details van rule nodes", "debug-mode": "Foutopsporingsmodus", - "singleton-mode": "Singleton-modus", "configuration": "Configuratie", "link": "Verbinden", "link-details": "Details van rule nodes links", diff --git a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json index ac27313248..bb98170a92 100644 --- a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json +++ b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json @@ -4474,7 +4474,6 @@ "deselect-all": "Odznacz wszystkie", "rulenode-details": "Szczegóły węzła reguły", "debug-mode": "Tryb debugowania", - "singleton-mode": "Tryb singletonowy", "configuration": "Konfiguracja", "link": "Połączenie", "link-details": "Szczegóły połączenia węzła reguły", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index ac302a78bb..bd2ff3c820 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -3968,7 +3968,6 @@ "deselect-all": "取消选择", "rulenode-details": "规则节点详情", "debug-mode": "调试模式", - "singleton-mode": "单例模式", "configuration": "配置", "link": "链接", "link-details": "规则节点链接详情", diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 37068909e0..c3240d7d26 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -1267,6 +1267,23 @@ pre.tb-highlight { .no-wrap { white-space: nowrap; } + + .tb-rounded-btn { + border-radius: 20px; + padding: 0 16px; + + &.active:not(:disabled) { + border-color: $primary !important; + } + + &:disabled, &:not(.active) { + background: rgba(0, 0, 0, 0.06); + } + + &:not(.active) { + color: rgba(0, 0, 0, 0.76); + } + } } /*************** From bc5d15553e6104e1523a10a2cbecb4f2c6a8240c Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 7 Nov 2024 11:44:54 +0200 Subject: [PATCH 15/77] refactoring --- .../rulechain/debug-strategy-button.component.ts | 2 +- .../pages/rulechain/rule-node-details.component.ts | 8 +++++--- ui-ngx/src/styles.scss | 11 ++++++----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.ts index 9a158d6a85..64194d8c93 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.ts @@ -99,7 +99,7 @@ export class DebugStrategyButtonComponent implements ControlValueAccessor { {}, {}, {}, true); debugStrategyPopover.tbComponentRef.instance.popover = debugStrategyPopover; - debugStrategyPopover.tbComponentRef.instance.onStrategyApplied.subscribe((strategy: DebugStrategy) => { + debugStrategyPopover.tbComponentRef.instance.onStrategyApplied.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((strategy: DebugStrategy) => { this.debugStrategyFormControl.patchValue(strategy); this.cdr.markForCheck(); debugStrategyPopover.hide(); diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts index ead0acb880..02fa18bd83 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts @@ -29,7 +29,7 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { DebugStrategy, FcRuleNode, RuleNodeType } from '@shared/models/rule-node.models'; +import { FcRuleNode, RuleNodeType } from '@shared/models/rule-node.models'; import { EntityType } from '@shared/models/entity-type.models'; import { Subject } from 'rxjs'; import { RuleNodeConfigComponent } from './rule-node-config.component'; @@ -92,7 +92,7 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O if (this.ruleNode) { this.ruleNodeFormGroup = this.fb.group({ name: [this.ruleNode.name, [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*'), Validators.maxLength(255)]], - debugStrategy: [this.ruleNode.debugStrategy ?? DebugStrategy.DISABLED], + debugStrategy: [this.ruleNode.debugStrategy], singletonMode: [this.ruleNode.singletonMode, []], configuration: [this.ruleNode.configuration, [Validators.required]], additionalInfo: this.fb.group( @@ -171,7 +171,9 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O if ($event) { $event.stopPropagation(); } - this.ruleNodeFormGroup.get('singletonMode').patchValue(!this.ruleNodeFormGroup.get('singletonMode').value); + const singleModeControl = this.ruleNodeFormGroup.get('singletonMode'); + singleModeControl.patchValue(!singleModeControl.value); + singleModeControl.markAsDirty(); } openRuleChain($event: Event) { diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index c3240d7d26..50172eaa43 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -1272,17 +1272,18 @@ pre.tb-highlight { border-radius: 20px; padding: 0 16px; + &:not(.active) { + color: rgba(0, 0, 0, 0.76); + background: rgba(0, 0, 0, 0.06); + } + &.active:not(:disabled) { border-color: $primary !important; } - &:disabled, &:not(.active) { + &:disabled { background: rgba(0, 0, 0, 0.06); } - - &:not(.active) { - color: rgba(0, 0, 0, 0.76); - } } } From 0a00b3939849853857c4fa2402ec6e86a570a62e Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 7 Nov 2024 16:26:38 +0200 Subject: [PATCH 16/77] major refactoring --- ui-ngx/src/app/core/auth/auth.models.ts | 1 + .../rulechain/debug-duration-left.pipe.ts | 9 ++- .../debug-strategy-button.component.html | 6 +- .../debug-strategy-button.component.ts | 6 +- .../debug-strategy-panel.component.html | 19 +++-- .../debug-strategy-panel.component.ts | 7 +- .../rule-node-details.component.html | 8 +- .../src/app/shared/models/rule-node.models.ts | 2 +- .../pipe/milliseconds-to-time-string.pipe.ts | 75 ++++++++----------- .../assets/locale/locale.constant-en_US.json | 6 +- 10 files changed, 74 insertions(+), 65 deletions(-) diff --git a/ui-ngx/src/app/core/auth/auth.models.ts b/ui-ngx/src/app/core/auth/auth.models.ts index cfc3fed4ba..e4d05e7d21 100644 --- a/ui-ngx/src/app/core/auth/auth.models.ts +++ b/ui-ngx/src/app/core/auth/auth.models.ts @@ -28,6 +28,7 @@ export interface SysParamsState { userSettings: UserSettings; maxResourceSize: number; maxRuleNodeDebugDurationMinutes: number; + ruleChainDebugPerTenantLimitsConfiguration?: string; } export interface SysParams extends SysParamsState { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts b/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts index 17176bd4af..7086bb504d 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts @@ -16,6 +16,8 @@ import { Pipe, PipeTransform } from '@angular/core'; import { MINUTE } from '@shared/models/time/time.models'; +import { TranslateService } from '@ngx-translate/core'; +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; @Pipe({ name: 'debugDurationLeft', @@ -23,9 +25,12 @@ import { MINUTE } from '@shared/models/time/time.models'; standalone: true, }) export class DebugDurationLeftPipe implements PipeTransform { + + constructor(private translate: TranslateService, private millisecondsToTimeString: MillisecondsToTimeStringPipe) { + } transform(lastUpdateTs: number, maxRuleNodeDebugDurationMinutes: number): string { - const minutes = Math.max(0, Math.floor(this.getDebugTime(lastUpdateTs, maxRuleNodeDebugDurationMinutes) / MINUTE)); - return `${minutes} min left`; + const time = this.millisecondsToTimeString.transform(this.getDebugTime(lastUpdateTs, maxRuleNodeDebugDurationMinutes), true, true); + return `${time} ` + this.translate.instant('common.left'); } getDebugTime(lastUpdateTs: number, maxRuleNodeDebugDurationMinutes: number): number { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.html index 800f54ae3e..ea94a99c74 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.html @@ -16,7 +16,7 @@ -->
diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts index 56f76b33fe..c68ecc3d1e 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -358,7 +358,7 @@ export enum DebugStrategy { DISABLED = 'DISABLED', ALL_EVENTS = 'ALL_EVENTS', ONLY_FAILURE_EVENTS = 'ONLY_FAILURE_EVENTS', - ALL_WITH_FAILURES = 'ALL_WITH_FAILURES', + ALL_THEN_ONLY_FAILURE_EVENTS = 'ALL_THEN_ONLY_FAILURE_EVENTS', } export interface FcRuleEdge extends FcEdge { diff --git a/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts b/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts index c767be3dc5..d7c5902898 100644 --- a/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts @@ -24,53 +24,42 @@ export class MillisecondsToTimeStringPipe implements PipeTransform { constructor(private translate: TranslateService) { } + transform(millseconds: number, shortFormat = false, onlyFirstDigit = false): string { + const { days, hours, minutes, seconds } = this.extractTimeUnits(millseconds); + return this.formatTimeString(days, hours, minutes, seconds, shortFormat, onlyFirstDigit); + } - transform(millseconds: number, shortFormat = false): string { - let seconds = Math.floor(millseconds / 1000); + private extractTimeUnits(milliseconds: number): { days: number; hours: number; minutes: number; seconds: number } { + const seconds = Math.floor(milliseconds / 1000); const days = Math.floor(seconds / 86400); - let hours = Math.floor((seconds % 86400) / 3600); - let minutes = Math.floor(((seconds % 86400) % 3600) / 60); - seconds = seconds % 60; + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor(((seconds % 86400) % 3600) / 60); + return { days, hours, minutes, seconds: seconds % 60 }; + } + + private formatTimeString( + days: number, + hours: number, + minutes: number, + seconds: number, + shortFormat: boolean, + onlyFirstDigit: boolean + ): string { + const timeUnits = [ + { value: days, key: 'days', shortKey: 'short.days' }, + { value: hours, key: 'hours', shortKey: 'short.hours' }, + { value: minutes, key: 'minutes', shortKey: 'short.minutes' }, + { value: seconds, key: 'seconds', shortKey: 'short.seconds' } + ]; + let timeString = ''; - if (shortFormat) { - if (days > 0) { - timeString += this.translate.instant('timewindow.short.days', {days}); - } - if (hours > 0) { - timeString += this.translate.instant('timewindow.short.hours', {hours}); - } - if (minutes > 0) { - timeString += this.translate.instant('timewindow.short.minutes', {minutes}); - } - if (seconds > 0) { - timeString += this.translate.instant('timewindow.short.seconds', {seconds}); - } - if (!timeString.length) { - timeString += this.translate.instant('timewindow.short.seconds', {seconds: 0}); - } - } else { - if (days > 0) { - timeString += this.translate.instant('timewindow.days', {days}); - } - if (hours > 0) { - if (timeString.length === 0 && hours === 1) { - hours = 0; - } - timeString += this.translate.instant('timewindow.hours', {hours}); - } - if (minutes > 0) { - if (timeString.length === 0 && minutes === 1) { - minutes = 0; - } - timeString += this.translate.instant('timewindow.minutes', {minutes}); - } - if (seconds > 0) { - if (timeString.length === 0 && seconds === 1) { - seconds = 0; - } - timeString += this.translate.instant('timewindow.seconds', {seconds}); + for (const { value, key, shortKey } of timeUnits) { + if (value > 0) { + timeString += this.translate.instant(shortFormat ? `timewindow.${shortKey}` : `timewindow.${key}`, { [key]: value }); + if (onlyFirstDigit) return timeString; } } - return timeString; + + return timeString.length > 0 ? timeString : this.translate.instant('timewindow.short.seconds', { seconds: 0 }); } } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 6185257663..161a5194bc 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1004,6 +1004,7 @@ "all": "All", "hint": { "main": "All node debug messages rate limited with:", + "main-limited": "All node debug messages will be rate-limited, with a maximum of {{msg}} messages allowed per {{sec}} seconds.", "on-failure": "Save all failure debug events without time limit.", "all-messages": "Save all debug events during time limit." } @@ -1038,11 +1039,13 @@ "enter-password": "Enter password", "enter-search": "Enter search", "created-time": "Created time", + "disabled": "Disabled", "loading": "Loading...", "proceed": "Proceed", "open-details-page": "Open details page", "not-found": "Not found", - "documentation": "Documentation" + "documentation": "Documentation", + "left": "left" }, "content-type": { "json": "Json", @@ -2084,7 +2087,6 @@ "column": "Column", "row": "Row" }, - "disabled": "Disabled", "edge": { "edge": "Edge", "edge-instances": "Edge instances", From 107c95fbe2d215c589a95b58c101478e893cf872 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 7 Nov 2024 16:39:46 +0200 Subject: [PATCH 17/77] refactoring --- .../home/pages/rulechain/debug-duration-left.pipe.ts | 3 ++- .../shared/pipe/milliseconds-to-time-string.pipe.ts | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts b/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts index 7086bb504d..d6615da476 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts @@ -28,12 +28,13 @@ export class DebugDurationLeftPipe implements PipeTransform { constructor(private translate: TranslateService, private millisecondsToTimeString: MillisecondsToTimeStringPipe) { } + transform(lastUpdateTs: number, maxRuleNodeDebugDurationMinutes: number): string { const time = this.millisecondsToTimeString.transform(this.getDebugTime(lastUpdateTs, maxRuleNodeDebugDurationMinutes), true, true); return `${time} ` + this.translate.instant('common.left'); } - getDebugTime(lastUpdateTs: number, maxRuleNodeDebugDurationMinutes: number): number { + private getDebugTime(lastUpdateTs: number, maxRuleNodeDebugDurationMinutes: number): number { const maxDuration = maxRuleNodeDebugDurationMinutes * MINUTE; const updateDuration = lastUpdateTs ? new Date().getTime() - lastUpdateTs : 0; const leftTime = maxDuration - updateDuration; diff --git a/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts b/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts index d7c5902898..d00fd3f3fc 100644 --- a/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts @@ -16,6 +16,7 @@ import { Pipe, PipeTransform } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; +import { DAY, HOUR, MINUTE, SECOND } from '@shared/models/time/time.models'; @Pipe({ name: 'milliSecondsToTimeString' @@ -24,17 +25,18 @@ export class MillisecondsToTimeStringPipe implements PipeTransform { constructor(private translate: TranslateService) { } + transform(millseconds: number, shortFormat = false, onlyFirstDigit = false): string { const { days, hours, minutes, seconds } = this.extractTimeUnits(millseconds); return this.formatTimeString(days, hours, minutes, seconds, shortFormat, onlyFirstDigit); } private extractTimeUnits(milliseconds: number): { days: number; hours: number; minutes: number; seconds: number } { - const seconds = Math.floor(milliseconds / 1000); - const days = Math.floor(seconds / 86400); - const hours = Math.floor((seconds % 86400) / 3600); - const minutes = Math.floor(((seconds % 86400) % 3600) / 60); - return { days, hours, minutes, seconds: seconds % 60 }; + const days = Math.floor(milliseconds / DAY); + const hours = Math.floor((milliseconds % DAY) / HOUR); + const minutes = Math.floor((milliseconds % HOUR) / MINUTE); + const seconds = Math.floor((milliseconds % MINUTE) / SECOND); + return { days, hours, minutes, seconds }; } private formatTimeString( From 76351f793a2464e11b98f3c6b7ab5e955a2d5a01 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 7 Nov 2024 16:40:52 +0200 Subject: [PATCH 18/77] refactoring --- .../src/app/shared/pipe/milliseconds-to-time-string.pipe.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts b/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts index d00fd3f3fc..1774d70fcb 100644 --- a/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts @@ -26,8 +26,8 @@ export class MillisecondsToTimeStringPipe implements PipeTransform { constructor(private translate: TranslateService) { } - transform(millseconds: number, shortFormat = false, onlyFirstDigit = false): string { - const { days, hours, minutes, seconds } = this.extractTimeUnits(millseconds); + transform(millSeconds: number, shortFormat = false, onlyFirstDigit = false): string { + const { days, hours, minutes, seconds } = this.extractTimeUnits(millSeconds); return this.formatTimeString(days, hours, minutes, seconds, shortFormat, onlyFirstDigit); } From 94f563f1857ddf863d65e21e1280e510b23c79cf Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 7 Nov 2024 16:41:09 +0200 Subject: [PATCH 19/77] refactoring --- .../src/app/shared/pipe/milliseconds-to-time-string.pipe.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts b/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts index 1774d70fcb..de25f15514 100644 --- a/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts +++ b/ui-ngx/src/app/shared/pipe/milliseconds-to-time-string.pipe.ts @@ -26,8 +26,8 @@ export class MillisecondsToTimeStringPipe implements PipeTransform { constructor(private translate: TranslateService) { } - transform(millSeconds: number, shortFormat = false, onlyFirstDigit = false): string { - const { days, hours, minutes, seconds } = this.extractTimeUnits(millSeconds); + transform(milliSeconds: number, shortFormat = false, onlyFirstDigit = false): string { + const { days, hours, minutes, seconds } = this.extractTimeUnits(milliSeconds); return this.formatTimeString(days, hours, minutes, seconds, shortFormat, onlyFirstDigit); } From c15e8e7fe74baafce3ce3b81080d1557b99a0b27 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Thu, 7 Nov 2024 17:18:43 +0200 Subject: [PATCH 20/77] refactoring --- .../home/pages/rulechain/rule-node-details.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts index 02fa18bd83..1c0712c778 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts @@ -29,7 +29,7 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { FcRuleNode, RuleNodeType } from '@shared/models/rule-node.models'; +import { DebugStrategy, FcRuleNode, RuleNodeType } from '@shared/models/rule-node.models'; import { EntityType } from '@shared/models/entity-type.models'; import { Subject } from 'rxjs'; import { RuleNodeConfigComponent } from './rule-node-config.component'; @@ -92,7 +92,7 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O if (this.ruleNode) { this.ruleNodeFormGroup = this.fb.group({ name: [this.ruleNode.name, [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*'), Validators.maxLength(255)]], - debugStrategy: [this.ruleNode.debugStrategy], + debugStrategy: [this.ruleNode.debugStrategy ?? DebugStrategy.DISABLED], singletonMode: [this.ruleNode.singletonMode, []], configuration: [this.ruleNode.configuration, [Validators.required]], additionalInfo: this.fb.group( From 637fe2a258856172e2aaa2fd86cde14a57a55478 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Mon, 11 Nov 2024 22:15:23 +0100 Subject: [PATCH 21/77] Used debugFailures and debugAll params instead of DebugStrategies --- .../rule_chains/edge_root_rule_chain.json | 27 +- .../device_profile/rule_chain_template.json | 21 +- .../tenant/rule_chains/root_rule_chain.json | 30 ++- .../main/data/upgrade/3.8.1/schema_update.sql | 6 +- .../actors/ruleChain/DefaultTbContext.java | 17 +- .../RuleChainActorMessageProcessor.java | 9 +- .../RuleNodeActorMessageProcessor.java | 3 +- .../BaseRuleChainMetadataConstructor.java | 5 +- .../actors/rule/DefaultTbContextTest.java | 251 +++++++----------- .../server/edge/AbstractEdgeTest.java | 6 +- .../server/edge/RuleChainEdgeTest.java | 3 +- ...AbstractRuleEngineFlowIntegrationTest.java | 11 +- ...actRuleEngineLifecycleIntegrationTest.java | 3 +- .../housekeeper/HousekeeperServiceTest.java | 5 +- .../sync/ie/ExportImportServiceSqlTest.java | 9 +- .../service/sync/vc/VersionControlTest.java | 9 +- .../common/data/rule/DebugStrategy.java | 70 ----- .../server/common/data/rule/RuleNode.java | 19 +- .../common/data/rule/RuleNodeDebugUtil.java | 45 ++++ .../DefaultTenantProfileConfiguration.java | 5 +- common/edge-api/src/main/proto/edge.proto | 11 +- .../server/dao/model/ModelConstants.java | 3 +- .../server/dao/model/sql/RuleNodeEntity.java | 20 +- .../server/dao/rule/BaseRuleChainService.java | 21 +- .../main/resources/sql/schema-entities.sql | 4 +- .../src/main/resources/root_rule_chain.json | 54 ++-- .../resources/MqttRuleNodeTestMetadata.json | 9 +- .../RpcResponseRuleChainMetadata.json | 9 +- 28 files changed, 326 insertions(+), 359 deletions(-) delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/rule/DebugStrategy.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNodeDebugUtil.java diff --git a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json index 04a5f52f4e..05dff1678f 100644 --- a/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json +++ b/application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json @@ -20,7 +20,8 @@ }, "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", "name": "Device Profile Node", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "configuration": { "persistAlarmRulesState": false, "fetchAlarmRulesStateOnStart": false @@ -34,7 +35,8 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "configuration": { "defaultTTL": 0 }, @@ -47,7 +49,8 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "configurationVersion": 2, "configuration": { "scope": "CLIENT_SCOPE", @@ -64,7 +67,8 @@ }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "Message Type Switch", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "configuration": { "version": 0 }, @@ -77,7 +81,8 @@ }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log RPC from Device", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "configuration": { "scriptLang": "TBEL", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", @@ -92,7 +97,8 @@ }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log Other", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "configuration": { "scriptLang": "TBEL", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", @@ -107,7 +113,8 @@ }, "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "name": "RPC Call Request", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "configuration": { "timeoutInSeconds": 60 }, @@ -120,7 +127,8 @@ }, "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "name": "Push to cloud", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "configuration": { "scope": "SERVER_SCOPE" }, @@ -133,7 +141,8 @@ }, "type": "org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode", "name": "Push to cloud", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "configuration": { "scope": "SERVER_SCOPE" }, diff --git a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json index b005331862..3e265591de 100644 --- a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json +++ b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json @@ -19,7 +19,8 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "configuration": { "defaultTTL": 0 } @@ -31,7 +32,8 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "configurationVersion": 2, "configuration": { "scope": "CLIENT_SCOPE", @@ -47,7 +49,8 @@ }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "Message Type Switch", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "configuration": { "version": 0 } @@ -59,7 +62,8 @@ }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log RPC from Device", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "configuration": { "scriptLang": "TBEL", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", @@ -73,7 +77,8 @@ }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log Other", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "configuration": { "scriptLang": "TBEL", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", @@ -87,7 +92,8 @@ }, "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "name": "RPC Call Request", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "configuration": { "timeoutInSeconds": 60 } @@ -100,7 +106,8 @@ }, "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", "name": "Device Profile Node", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "configuration": { "persistAlarmRulesState": false, "fetchAlarmRulesStateOnStart": false diff --git a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json index 3b9898d0b1..4a799cf957 100644 --- a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json +++ b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json @@ -18,7 +18,9 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, + "debugAllUntil": 0, "configuration": { "defaultTTL": 0 } @@ -30,7 +32,9 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Client Attributes", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, + "debugAllUntil": 0, "configurationVersion": 2, "configuration": { "scope": "CLIENT_SCOPE", @@ -46,7 +50,9 @@ }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "Message Type Switch", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, + "debugAllUntil": 0, "configuration": { "version": 0 } @@ -58,7 +64,9 @@ }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log RPC from Device", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, + "debugAllUntil": 0, "configuration": { "scriptLang": "TBEL", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", @@ -72,7 +80,9 @@ }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log Other", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, + "debugAllUntil": 0, "configuration": { "scriptLang": "TBEL", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", @@ -86,7 +96,9 @@ }, "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "name": "RPC Call Request", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, + "debugAllUntil": 0, "configuration": { "timeoutInSeconds": 60 } @@ -99,7 +111,9 @@ }, "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", "name": "Device Profile Node", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, + "debugAllUntil": 0, "configuration": { "persistAlarmRulesState": false, "fetchAlarmRulesStateOnStart": false @@ -140,4 +154,4 @@ ], "ruleChainConnections": null } -} \ No newline at end of file +} diff --git a/application/src/main/data/upgrade/3.8.1/schema_update.sql b/application/src/main/data/upgrade/3.8.1/schema_update.sql index dab9505d36..cbe0e6103a 100644 --- a/application/src/main/data/upgrade/3.8.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.8.1/schema_update.sql @@ -28,9 +28,9 @@ UPDATE tb_user SET additional_info = (additional_info::jsonb - 'lastLoginTs' - ' -- UPDATE RULE NODE DEBUG MODE TO DEBUG STRATEGY START ALTER TABLE rule_node - ADD COLUMN IF NOT EXISTS debug_strategy varchar(32) DEFAULT 'DISABLED'; + ADD COLUMN IF NOT EXISTS debug_failures boolean DEFAULT false; ALTER TABLE rule_node - ADD COLUMN IF NOT EXISTS last_update_ts bigint NOT NULL DEFAULT extract(epoch from now()) * 1000; + ADD COLUMN IF NOT EXISTS debug_all_until bigint NOT NULL DEFAULT 0; DO $$ BEGIN @@ -38,7 +38,7 @@ $$ FROM information_schema.columns WHERE table_name = 'rule_node' AND column_name = 'debug_mode') THEN UPDATE rule_node - SET debug_strategy = CASE WHEN debug_mode = true THEN 'ALL_EVENTS' ELSE 'DISABLED' END; + SET debug_all_until = CASE WHEN debug_mode = true THEN extract(epoch from now() + 3600) * 1000 ELSE 0 END; ALTER TABLE rule_node DROP COLUMN debug_mode; END IF; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 119d1d2900..000c260769 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -63,8 +63,8 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.rule.RuleNodeDebugUtil; import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbActorMsg; @@ -1002,22 +1002,11 @@ public class DefaultTbContext implements TbContext { private void persistDebugOutput(TbMsg msg, Set relationTypes, Throwable error, String failureMessage) { RuleNode ruleNode = nodeCtx.getSelf(); - DebugStrategy debugStrategy = ruleNode.getDebugStrategy(); - if (debugStrategy.shouldPersistDebugOutputForAllEvents(ruleNode.getLastUpdateTs(), msg.getTs(), getMaxRuleNodeDebugDurationMinutes())) { + if (RuleNodeDebugUtil.isDebugAllAvailable(ruleNode)) { relationTypes.forEach(relationType -> mainCtx.persistDebugOutput(getTenantId(), ruleNode.getId(), msg, relationType, error, failureMessage)); - } else if (debugStrategy.shouldPersistDebugForFailureEvent(relationTypes)) { + } else if (RuleNodeDebugUtil.isDebugFailuresAvailable(ruleNode, relationTypes)) { mainCtx.persistDebugOutput(getTenantId(), ruleNode.getId(), msg, TbNodeConnectionType.FAILURE, error, failureMessage); } } - private int getMaxRuleNodeDebugDurationMinutes() { - if (nodeCtx.getSelf().getDebugStrategy().isHasDuration()) { - var configuration = mainCtx.getTenantProfileCache() - .get(getTenantId()).getProfileData().getConfiguration(); - int systemMaxRuleNodeDebugModeDurationMinutes = mainCtx.getMaxRuleNodeDebugModeDurationMinutes(); - return configuration.getMaxRuleNodeDebugModeDurationMinutes(systemMaxRuleNodeDebugModeDurationMinutes); - } - return 0; - } - } diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java index 01caa50860..636ea3fd07 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.rule.RuleNodeDebugUtil; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.plugin.RuleNodeUpdatedMsg; @@ -255,13 +256,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor connections) { + void givenDebugFailuresEventsAndConnections_whenTellNext_thenVerifyDebugOutputPersisted(Set connections) { // GIVEN var callbackMock = mock(TbMsgCallback.class); var msg = getTbMsgWithCallback(callbackMock); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(DebugStrategy.ONLY_FAILURE_EVENTS); + ruleNode.setDebugFailures(true); + ruleNode.setDebugAllUntil(0); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); @@ -169,7 +166,7 @@ class DefaultTbContextTest { checkTellNextCommonLogic(callbackMock, connections, msg); } - private static Stream> givenDebugStrategyOnlyFailureEventsAndConnections_whenTellNext_thenVerifyDebugOutputPersisted() { + private static Stream> givenDebugFailuresEventsAndConnections_whenTellNext_thenVerifyDebugOutputPersisted() { return Stream.of( Collections.singleton(TbNodeConnectionType.FAILURE), Set.of(TbNodeConnectionType.FAILURE, TbNodeConnectionType.SUCCESS) @@ -178,13 +175,14 @@ class DefaultTbContextTest { @MethodSource @ParameterizedTest - void givenDebugStrategyDisabledAndConnections_whenTellNext_thenVerifyDebugOutputNotPersisted(Set connections) { + void givenDebugDisabledAndConnections_whenTellNext_thenVerifyDebugOutputNotPersisted(Set connections) { // GIVEN var callbackMock = mock(TbMsgCallback.class); var msg = getTbMsgWithCallback(callbackMock); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(DebugStrategy.DISABLED); + ruleNode.setDebugFailures(false); + ruleNode.setDebugAllUntil(0); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); @@ -198,7 +196,7 @@ class DefaultTbContextTest { checkTellNextCommonLogic(callbackMock, connections, msg); } - private static Stream> givenDebugStrategyDisabledAndConnections_whenTellNext_thenVerifyDebugOutputNotPersisted() { + private static Stream> givenDebugDisabledAndConnections_whenTellNext_thenVerifyDebugOutputNotPersisted() { return Stream.of( Collections.singleton(TbNodeConnectionType.FAILURE), Collections.singleton(TbNodeConnectionType.SUCCESS), @@ -208,18 +206,17 @@ class DefaultTbContextTest { @MethodSource @ParameterizedTest - void givenDebugStrategyAllEventsAndConnection_whenTellNext_thenVerifyDebugOutputPersisted(String connection) { + void givenDebugAllEventsAndConnection_whenTellNext_thenVerifyDebugOutputPersisted(String connection) { // GIVEN var callbackMock = mock(TbMsgCallback.class); var msg = getTbMsgWithCallback(callbackMock); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setLastUpdateTs(System.currentTimeMillis()); - ruleNode.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode.setDebugFailures(false); + ruleNode.setDebugAllUntil(getUntilTime()); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); - mockGetMaxRuleNodeDebugModeDurationMinutes(); // WHEN defaultTbContext.tellNext(msg, connection); @@ -227,30 +224,27 @@ class DefaultTbContextTest { // THEN then(nodeCtxMock).should().getChainActor(); then(nodeCtxMock).shouldHaveNoMoreInteractions(); - then(mainCtxMock).should().getTenantProfileCache(); - then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, connection, null, null); then(mainCtxMock).shouldHaveNoMoreInteractions(); checkTellNextCommonLogic(callbackMock, connection, msg); } - private static Stream givenDebugStrategyAllEventsAndConnection_whenTellNext_thenVerifyDebugOutputPersisted() { + private static Stream givenDebugAllEventsAndConnection_whenTellNext_thenVerifyDebugOutputPersisted() { return failureAndSuccessConnection(); } @Test - public void givenDebugStrategyAllEventsAndFailureAndSuccessConnection_whenTellNext_thenVerifyDebugOutputPersistedForAllEvents() { + public void givenDebugAllEventsAndFailureAndSuccessConnection_whenTellNext_thenVerifyDebugOutputPersistedForAllEvents() { // GIVEN var callbackMock = mock(TbMsgCallback.class); var msg = getTbMsgWithCallback(callbackMock); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setLastUpdateTs(System.currentTimeMillis()); - ruleNode.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode.setDebugFailures(false); + ruleNode.setDebugAllUntil(getUntilTime()); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); - mockGetMaxRuleNodeDebugModeDurationMinutes(); // WHEN Set connections = failureAndSuccessConnection().collect(Collectors.toSet()); @@ -259,8 +253,6 @@ class DefaultTbContextTest { // THEN then(nodeCtxMock).should().getChainActor(); then(nodeCtxMock).shouldHaveNoMoreInteractions(); - then(mainCtxMock).should().getTenantProfileCache(); - then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); var nodeConnectionsCaptor = ArgumentCaptor.forClass(String.class); int wantedNumberOfInvocations = connections.size(); then(mainCtxMock).should(times(wantedNumberOfInvocations)).persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), eq(msg), nodeConnectionsCaptor.capture(), nullable(Throwable.class), nullable(String.class)); @@ -272,18 +264,17 @@ class DefaultTbContextTest { @MethodSource @ParameterizedTest - void givenDebugStrategyAllThenOnlyFailureEventsAndConnection_whenTellNext_thenVerifyDebugOutputPersisted(String connection) { + void givenDebugAllThenOnlyFailureEventsAndConnection_whenTellNext_thenVerifyDebugOutputPersisted(String connection) { // GIVEN var callbackMock = mock(TbMsgCallback.class); var msg = getTbMsgWithCallback(callbackMock); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setLastUpdateTs(System.currentTimeMillis()); - ruleNode.setDebugStrategy(DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS); + ruleNode.setDebugFailures(false); + ruleNode.setDebugAllUntil(getUntilTime()); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); - mockGetMaxRuleNodeDebugModeDurationMinutes(); // WHEN defaultTbContext.tellNext(msg, connection); @@ -291,30 +282,27 @@ class DefaultTbContextTest { // THEN then(nodeCtxMock).should().getChainActor(); then(nodeCtxMock).shouldHaveNoMoreInteractions(); - then(mainCtxMock).should().getTenantProfileCache(); - then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, connection, null, null); then(mainCtxMock).shouldHaveNoMoreInteractions(); checkTellNextCommonLogic(callbackMock, connection, msg); } - private static Stream givenDebugStrategyAllThenOnlyFailureEventsAndConnection_whenTellNext_thenVerifyDebugOutputPersisted() { + private static Stream givenDebugAllThenOnlyFailureEventsAndConnection_whenTellNext_thenVerifyDebugOutputPersisted() { return failureAndSuccessConnection(); } @Test - public void givenDebugStrategyAllThenOnlyEventsAndFailureAndSuccessConnection_whenTellNext_thenVerifyDebugOutputPersistedForAllEvents() { + public void givenDebugAllThenOnlyEventsAndFailureAndSuccessConnection_whenTellNext_thenVerifyDebugOutputPersistedForAllEvents() { // GIVEN var callbackMock = mock(TbMsgCallback.class); var msg = getTbMsgWithCallback(callbackMock); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setLastUpdateTs(System.currentTimeMillis()); - ruleNode.setDebugStrategy(DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS); + ruleNode.setDebugFailures(true); + ruleNode.setDebugAllUntil(getUntilTime()); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); - mockGetMaxRuleNodeDebugModeDurationMinutes(); // WHEN Set connections = failureAndSuccessConnection().collect(Collectors.toSet()); @@ -323,8 +311,6 @@ class DefaultTbContextTest { // THEN then(nodeCtxMock).should().getChainActor(); then(nodeCtxMock).shouldHaveNoMoreInteractions(); - then(mainCtxMock).should().getTenantProfileCache(); - then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); var nodeConnectionsCaptor = ArgumentCaptor.forClass(String.class); int wantedNumberOfInvocations = connections.size(); then(mainCtxMock).should(times(wantedNumberOfInvocations)).persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), eq(msg), nodeConnectionsCaptor.capture(), nullable(Throwable.class), nullable(String.class)); @@ -339,12 +325,12 @@ class DefaultTbContextTest { } @Test - public void givenDebugStrategyOnlyFailureEventsAndFailureConnection_whenOutput_thenVerifyDebugOutputPersisted() { + public void givenDebugFailuresEventsAndFailureConnection_whenOutput_thenVerifyDebugOutputPersisted() { // GIVEN var msgMock = mock(TbMsg.class); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(DebugStrategy.ONLY_FAILURE_EVENTS); + ruleNode.setDebugFailures(true); given(msgMock.popFormStack()).willReturn(new TbMsgProcessingStackItem(RULE_CHAIN_ID, RULE_NODE_ID)); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); given(nodeCtxMock.getSelf()).willReturn(ruleNode); @@ -361,12 +347,12 @@ class DefaultTbContextTest { } @Test - public void givenDebugStrategyOnlyFailureEventsAndSuccessConnection_whenOutput_thenVerifyDebugOutputNotPersisted() { + public void givenDebugFailuresEventsAndSuccessConnection_whenOutput_thenVerifyDebugOutputNotPersisted() { // GIVEN var msgMock = mock(TbMsg.class); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(DebugStrategy.ONLY_FAILURE_EVENTS); + ruleNode.setDebugFailures(true); given(msgMock.popFormStack()).willReturn(new TbMsgProcessingStackItem(RULE_CHAIN_ID, RULE_NODE_ID)); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); @@ -382,12 +368,13 @@ class DefaultTbContextTest { @ParameterizedTest @ValueSource(strings = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE}) - void givenDebugStrategyDisabled_whenOutput_thenVerifyDebugOutputNotPersisted(String nodeConnection) { + void givenDebugDisabled_whenOutput_thenVerifyDebugOutputNotPersisted(String nodeConnection) { // GIVEN var msgMock = mock(TbMsg.class); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(DebugStrategy.DISABLED); + ruleNode.setDebugFailures(false); + ruleNode.setDebugAllUntil(0); given(msgMock.popFormStack()).willReturn(new TbMsgProcessingStackItem(RULE_CHAIN_ID, RULE_NODE_ID)); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); @@ -403,25 +390,23 @@ class DefaultTbContextTest { @ParameterizedTest @ValueSource(strings = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE}) - void givenDebugStrategyAllEvents_whenOutput_thenVerifyDebugOutputPersisted(String nodeConnection) { + void givenDebugAllEvents_whenOutput_thenVerifyDebugOutputPersisted(String nodeConnection) { // GIVEN var msgMock = mock(TbMsg.class); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode.setDebugFailures(false); + ruleNode.setDebugAllUntil(getUntilTime()); given(msgMock.popFormStack()).willReturn(new TbMsgProcessingStackItem(RULE_CHAIN_ID, RULE_NODE_ID)); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); - mockGetMaxRuleNodeDebugModeDurationMinutes(); // WHEN defaultTbContext.output(msgMock, nodeConnection); // THEN checkOutputCommonLogic(msgMock, nodeConnection); - then(mainCtxMock).should().getTenantProfileCache(); - then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, nodeConnection, null, null); then(mainCtxMock).shouldHaveNoMoreInteractions(); then(nodeCtxMock).shouldHaveNoMoreInteractions(); @@ -429,25 +414,23 @@ class DefaultTbContextTest { @ParameterizedTest @ValueSource(strings = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE}) - void givenDebugStrategyAllThenOnlyFailureEvents_whenOutput_thenVerifyDebugOutputPersisted(String nodeConnection) { + void givenDebugAllThenOnlyFailureEvents_whenOutput_thenVerifyDebugOutputPersisted(String nodeConnection) { // GIVEN var msgMock = mock(TbMsg.class); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode.setDebugFailures(false); + ruleNode.setDebugAllUntil(getUntilTime()); given(msgMock.popFormStack()).willReturn(new TbMsgProcessingStackItem(RULE_CHAIN_ID, RULE_NODE_ID)); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); - mockGetMaxRuleNodeDebugModeDurationMinutes(); // WHEN defaultTbContext.output(msgMock, nodeConnection); // THEN checkOutputCommonLogic(msgMock, nodeConnection); - then(mainCtxMock).should().getTenantProfileCache(); - then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msgMock, nodeConnection, null, null); then(mainCtxMock).shouldHaveNoMoreInteractions(); then(nodeCtxMock).shouldHaveNoMoreInteractions(); @@ -459,7 +442,8 @@ class DefaultTbContextTest { var msgMock = mock(TbMsg.class); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(DebugStrategy.DISABLED); + ruleNode.setDebugFailures(false); + ruleNode.setDebugAllUntil(0); given(msgMock.popFormStack()).willReturn(null); TbMsgCallback callbackMock = mock(TbMsgCallback.class); given(msgMock.getCallback()).willReturn(callbackMock); @@ -476,19 +460,18 @@ class DefaultTbContextTest { } @Test - public void givenEmptyStackAndDebugStrategyAllEvents_whenOutput_thenVerifyMsgAckAndDebugOutputPersisted() { + public void givenEmptyStackAndDebugAllEvents_whenOutput_thenVerifyMsgAckAndDebugOutputPersisted() { // GIVEN var msgMock = mock(TbMsg.class); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(DebugStrategy.ALL_EVENTS); - ruleNode.setLastUpdateTs(System.currentTimeMillis()); + ruleNode.setDebugFailures(false); + ruleNode.setDebugAllUntil(getUntilTime()); given(msgMock.popFormStack()).willReturn(null); TbMsgCallback callbackMock = mock(TbMsgCallback.class); given(msgMock.getCallback()).willReturn(callbackMock); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); - mockGetMaxRuleNodeDebugModeDurationMinutes(); // WHEN defaultTbContext.output(msgMock, TbNodeConnectionType.SUCCESS); @@ -502,19 +485,18 @@ class DefaultTbContextTest { } @Test - public void givenEmptyStackAndDebugStrategyAllThenOnlyFailureEvents_whenOutput_thenVerifyMsgAckAndDebugOutputPersisted() { + public void givenEmptyStackAndDebugAllThenOnlyFailureEvents_whenOutput_thenVerifyMsgAckAndDebugOutputPersisted() { // GIVEN var msgMock = mock(TbMsg.class); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS); - ruleNode.setLastUpdateTs(System.currentTimeMillis()); + ruleNode.setDebugFailures(true); + ruleNode.setDebugAllUntil(getUntilTime()); given(msgMock.popFormStack()).willReturn(null); TbMsgCallback callbackMock = mock(TbMsgCallback.class); given(msgMock.getCallback()).willReturn(callbackMock); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); - mockGetMaxRuleNodeDebugModeDurationMinutes(); // WHEN defaultTbContext.output(msgMock, TbNodeConnectionType.SUCCESS); @@ -528,13 +510,13 @@ class DefaultTbContextTest { } @Test - public void givenDebugStrategyOnlyFailureEvents_whenEnqueueForTellFailure_thenVerifyDebugOutputPersisted() { + public void givenDebugFailuresEvents_whenEnqueueForTellFailure_thenVerifyDebugOutputPersisted() { // GIVEN var msg = getTbMsgWithQueueName(); var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(DebugStrategy.ONLY_FAILURE_EVENTS); + ruleNode.setDebugFailures(true); var tbClusterServiceMock = mock(TbClusterService.class); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); @@ -561,13 +543,14 @@ class DefaultTbContextTest { } @Test - public void givenDebugStrategyDisabled_whenEnqueueForTellFailure_thenVerifyDebugOutputNotPersisted() { + public void givenDebugDisabled_whenEnqueueForTellFailure_thenVerifyDebugOutputNotPersisted() { // GIVEN var msg = getTbMsgWithQueueName(); var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(DebugStrategy.DISABLED); + ruleNode.setDebugFailures(false); + ruleNode.setDebugAllUntil(0); var tbClusterServiceMock = mock(TbClusterService.class); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); @@ -588,21 +571,20 @@ class DefaultTbContextTest { } @Test - public void givenDebugStrategyAllEvents_whenEnqueueForTellFailure_thenVerifyDebugOutputPersisted() { + public void givenDebugAllEvents_whenEnqueueForTellFailure_thenVerifyDebugOutputPersisted() { // GIVEN var msg = getTbMsgWithQueueName(); var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(DebugStrategy.ALL_EVENTS); - ruleNode.setLastUpdateTs(System.currentTimeMillis()); + ruleNode.setDebugFailures(false); + ruleNode.setDebugAllUntil(getUntilTime()); var tbClusterServiceMock = mock(TbClusterService.class); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi); given(mainCtxMock.getClusterService()).willReturn(tbClusterServiceMock); - mockGetMaxRuleNodeDebugModeDurationMinutes(); // WHEN defaultTbContext.enqueueForTellFailure(msg, EXCEPTION); @@ -618,8 +600,6 @@ class DefaultTbContextTest { .ignoringFields("id", "ctx") .isEqualTo(expectedTbMsg); then(mainCtxMock).should().getClusterService(); - then(mainCtxMock).should().getTenantProfileCache(); - then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); then(mainCtxMock).shouldHaveNoMoreInteractions(); then(tbClusterServiceMock).shouldHaveNoMoreInteractions(); } @@ -655,23 +635,20 @@ class DefaultTbContextTest { @MethodSource @ParameterizedTest - void givenDebugStrategyOptions_whenEnqueueForTellNext_thenVerifyDebugOutputPersistedOnlyForAllEventsDebugStrategy(DebugStrategy debugStrategy, String connectionType) { + void givenDebugOptions_whenEnqueueForTellNext_thenVerifyDebugOutputPersistedOnlyForDebugAll(boolean debugFailures, long debugAllUntil, String connectionType) { // GIVEN var msg = getTbMsgWithQueueName(); var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(debugStrategy); - ruleNode.setLastUpdateTs(System.currentTimeMillis()); + ruleNode.setDebugFailures(debugFailures); + ruleNode.setDebugAllUntil(debugAllUntil); var tbClusterServiceMock = mock(TbClusterService.class); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi); given(mainCtxMock.getClusterService()).willReturn(tbClusterServiceMock); - if (DebugStrategy.ALL_EVENTS.equals(debugStrategy) || DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS.equals(debugStrategy)) { - mockGetMaxRuleNodeDebugModeDurationMinutes(); - } // WHEN defaultTbContext.enqueueForTellNext(msg, connectionType); @@ -697,9 +674,7 @@ class DefaultTbContextTest { assertThat(simpleTbQueueCallback).isNotNull(); simpleTbQueueCallback.onSuccess(null); - if (DebugStrategy.ALL_EVENTS.equals(debugStrategy) || DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS.equals(debugStrategy)) { - then(mainCtxMock).should().getTenantProfileCache(); - then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); + if (debugAllUntil > 0) { ArgumentCaptor tbMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); then(mainCtxMock).should().persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), tbMsgCaptor.capture(), eq(connectionType), isNull(), isNull()); TbMsg actualTbMsg = tbMsgCaptor.getValue(); @@ -714,24 +689,21 @@ class DefaultTbContextTest { @MethodSource @ParameterizedTest - void givenDebugStrategyOptions_whenEnqueue_thenVerifyDebugOutputPersistedOnlyForAllEventsDebugStrategy(DebugStrategy debugStrategy) { + void givenDebugOptions_whenEnqueue_thenVerifyDebugOutputPersistedOnlyForDebugAll(boolean debugFailures, long debugAllUntil) { // GIVEN var msg = getTbMsgWithQueueName(); var tpi = new TopicPartitionInfo(DataConstants.MAIN_QUEUE_TOPIC, TENANT_ID, 0, true); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setQueueName(DataConstants.MAIN_QUEUE_NAME); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(debugStrategy); - ruleNode.setLastUpdateTs(System.currentTimeMillis()); + ruleNode.setDebugFailures(debugFailures); + ruleNode.setDebugAllUntil(debugAllUntil); var tbClusterServiceMock = mock(TbClusterService.class); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(mainCtxMock.resolve(any(ServiceType.class), anyString(), any(TenantId.class), any(EntityId.class))).willReturn(tpi); given(mainCtxMock.getClusterService()).willReturn(tbClusterServiceMock); - if (DebugStrategy.ALL_EVENTS.equals(debugStrategy) || DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS.equals(debugStrategy)) { - mockGetMaxRuleNodeDebugModeDurationMinutes(); - } Consumer onFailure = mock(Consumer.class); Runnable onSuccess = mock(Runnable.class); @@ -760,9 +732,7 @@ class DefaultTbContextTest { assertThat(simpleTbQueueCallback).isNotNull(); simpleTbQueueCallback.onSuccess(null); - if (debugStrategy.isHasDuration()) { - then(mainCtxMock).should().getTenantProfileCache(); - then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); + if (debugAllUntil > 0) { then(mainCtxMock).should().persistDebugOutput(eq(TENANT_ID), eq(RULE_NODE_ID), eq(msg), eq(TbNodeConnectionType.TO_ROOT_RULE_CHAIN), nullable(Throwable.class), nullable(String.class)); } then(mainCtxMock).should().getClusterService(); @@ -771,12 +741,13 @@ class DefaultTbContextTest { } @Test - public void givenDebugStrategyOnlyFailures_whenTellFailure_thenVerifyDebugOutputPersisted() { + public void givenDebugFailuress_whenTellFailure_thenVerifyDebugOutputPersisted() { // GIVEN var msg = getTbMsg(); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(DebugStrategy.ONLY_FAILURE_EVENTS); + ruleNode.setDebugFailures(true); + ruleNode.setDebugAllUntil(0); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); @@ -801,12 +772,13 @@ class DefaultTbContextTest { } @Test - public void givenDebugStrategyDisabled_whenTellFailure_thenVerifyDebugOutputNotPersisted() { + public void givenDebugDisabled_whenTellFailure_thenVerifyDebugOutputNotPersisted() { // GIVEN var msg = getTbMsg(); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(DebugStrategy.DISABLED); + ruleNode.setDebugFailures(false); + ruleNode.setDebugAllUntil(0); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); @@ -829,17 +801,16 @@ class DefaultTbContextTest { } @Test - public void givenDebugStrategyAllEvents_whenTellFailure_thenVerifyDebugOutputPersisted() { + public void givenDebugAllEvents_whenTellFailure_thenVerifyDebugOutputPersisted() { // GIVEN var msg = getTbMsg(); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setDebugStrategy(DebugStrategy.ALL_EVENTS); - ruleNode.setLastUpdateTs(System.currentTimeMillis()); + ruleNode.setDebugFailures(false); + ruleNode.setDebugAllUntil(getUntilTime()); given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); - mockGetMaxRuleNodeDebugModeDurationMinutes(); // WHEN defaultTbContext.tellFailure(msg, EXCEPTION); @@ -856,15 +827,14 @@ class DefaultTbContextTest { then(chainActorMock).shouldHaveNoMoreInteractions(); then(nodeCtxMock).should().getChainActor(); then(mainCtxMock).should().persistDebugOutput(TENANT_ID, RULE_NODE_ID, msg, TbNodeConnectionType.FAILURE, EXCEPTION, null); - then(mainCtxMock).should().getTenantProfileCache(); - then(mainCtxMock).should().getMaxRuleNodeDebugModeDurationMinutes(); then(mainCtxMock).shouldHaveNoMoreInteractions(); then(nodeCtxMock).shouldHaveNoMoreInteractions(); } @MethodSource @ParameterizedTest - void givenDebugStrategyAndConnectionAndPersistedResultOptions_whenTellNext_thenVerifyDebugOutputPersistence(DebugStrategy debugStrategy, + void givenDebugFailuresAndDebugAllAndConnectionAndPersistedResultOptions_whenTellNext_thenVerifyDebugOutputPersistence(boolean debugFailures, + long debugAllUntil, String connection, boolean shouldPersist, boolean shouldPersistAfterDurationTime) { @@ -873,16 +843,13 @@ class DefaultTbContextTest { var msg = getTbMsgWithCallback(callbackMock); var ruleNode = new RuleNode(RULE_NODE_ID); ruleNode.setRuleChainId(RULE_CHAIN_ID); - ruleNode.setLastUpdateTs(System.currentTimeMillis()); - ruleNode.setDebugStrategy(debugStrategy); + ruleNode.setDebugFailures(debugFailures); + ruleNode.setDebugAllUntil(debugAllUntil); if (shouldPersist) { given(nodeCtxMock.getTenantId()).willReturn(TENANT_ID); } given(nodeCtxMock.getSelf()).willReturn(ruleNode); given(nodeCtxMock.getChainActor()).willReturn(chainActorMock); - if (debugStrategy.isHasDuration()) { - mockGetMaxRuleNodeDebugModeDurationMinutes(); - } // WHEN defaultTbContext.tellNext(msg, connection); @@ -894,9 +861,7 @@ class DefaultTbContextTest { // GIVEN Mockito.clearInvocations(mainCtxMock); - if (debugStrategy.isHasDuration()) { - mockGetMaxRuleNodeDebugModeDurationMinutes(0); - } + ruleNode.setDebugAllUntil(0); // WHEN defaultTbContext.tellNext(msg, connection); @@ -956,34 +921,34 @@ class DefaultTbContextTest { simpleTbQueueCallback.onSuccess(null); } - private static Stream givenDebugStrategyOptions_whenEnqueueForTellNext_thenVerifyDebugOutputPersistedOnlyForAllEventsDebugStrategy() { + private static Stream givenDebugOptions_whenEnqueueForTellNext_thenVerifyDebugOutputPersistedOnlyForDebugAll() { return Stream.of( - Arguments.of(DebugStrategy.ALL_EVENTS, TbNodeConnectionType.OTHER), - Arguments.of(DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS, TbNodeConnectionType.OTHER), - Arguments.of(DebugStrategy.ONLY_FAILURE_EVENTS, TbNodeConnectionType.TRUE), - Arguments.of(DebugStrategy.DISABLED, TbNodeConnectionType.FALSE) + Arguments.of(false, getUntilTime(), TbNodeConnectionType.OTHER), + Arguments.of(true, getUntilTime(), TbNodeConnectionType.OTHER), + Arguments.of(true, 0, TbNodeConnectionType.TRUE), + Arguments.of(false, 0, TbNodeConnectionType.FALSE) ); } - private static Stream givenDebugStrategyOptions_whenEnqueue_thenVerifyDebugOutputPersistedOnlyForAllEventsDebugStrategy() { + private static Stream givenDebugOptions_whenEnqueue_thenVerifyDebugOutputPersistedOnlyForDebugAll() { return Stream.of( - Arguments.of(DebugStrategy.ALL_EVENTS), - Arguments.of(DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS), - Arguments.of(DebugStrategy.ONLY_FAILURE_EVENTS), - Arguments.of(DebugStrategy.DISABLED) + Arguments.of(false, getUntilTime()), + Arguments.of(true, getUntilTime()), + Arguments.of(true, 0), + Arguments.of(false, 0) ); } - private static Stream givenDebugStrategyAndConnectionAndPersistedResultOptions_whenTellNext_thenVerifyDebugOutputPersistence() { + private static Stream givenDebugFailuresAndDebugAllAndConnectionAndPersistedResultOptions_whenTellNext_thenVerifyDebugOutputPersistence() { return Stream.of( - Arguments.of(DebugStrategy.ALL_EVENTS, TbNodeConnectionType.SUCCESS, true, false), - Arguments.of(DebugStrategy.ALL_EVENTS, TbNodeConnectionType.FAILURE, true, false), - Arguments.of(DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS, TbNodeConnectionType.SUCCESS, true, false), - Arguments.of(DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS, TbNodeConnectionType.FAILURE, true, true), - Arguments.of(DebugStrategy.ONLY_FAILURE_EVENTS, TbNodeConnectionType.SUCCESS, false, false), - Arguments.of(DebugStrategy.ONLY_FAILURE_EVENTS, TbNodeConnectionType.FAILURE, true, true), - Arguments.of(DebugStrategy.DISABLED, TbNodeConnectionType.SUCCESS, false, false), - Arguments.of(DebugStrategy.DISABLED, TbNodeConnectionType.FAILURE, false, false) + Arguments.of(false, getUntilTime(), TbNodeConnectionType.SUCCESS, true, false), + Arguments.of(false, getUntilTime(), TbNodeConnectionType.FAILURE, true, false), + Arguments.of(true, getUntilTime(), TbNodeConnectionType.SUCCESS, true, false), + Arguments.of(true, getUntilTime(), TbNodeConnectionType.FAILURE, true, true), + Arguments.of(true, 0, TbNodeConnectionType.SUCCESS, false, false), + Arguments.of(true, 0, TbNodeConnectionType.FAILURE, true, true), + Arguments.of(false, 0, TbNodeConnectionType.SUCCESS, false, false), + Arguments.of(false, 0, TbNodeConnectionType.FAILURE, false, false) ); } @@ -999,21 +964,11 @@ class DefaultTbContextTest { return TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, TENANT_ID, TbMsgMetaData.EMPTY, TbMsg.EMPTY_STRING); } - private void mockGetMaxRuleNodeDebugModeDurationMinutes() { - mockGetMaxRuleNodeDebugModeDurationMinutes(15); + private static long getUntilTime() { + return getUntilTime(15); } - private void mockGetMaxRuleNodeDebugModeDurationMinutes(int maxRuleNodeDebugModeDurationMinutes) { - var tbTenantProfileCacheMock = mock(TbTenantProfileCache.class); - var tenantProfileMock = mock(TenantProfile.class); - var tenantProfileDataMock = mock(TenantProfileData.class); - var tenantProfileConfigurationMock = mock(TenantProfileConfiguration.class); - - given(mainCtxMock.getTenantProfileCache()).willReturn(tbTenantProfileCacheMock); - given(tbTenantProfileCacheMock.get(TENANT_ID)).willReturn(tenantProfileMock); - given(tenantProfileMock.getProfileData()).willReturn(tenantProfileDataMock); - given(tenantProfileDataMock.getConfiguration()).willReturn(tenantProfileConfigurationMock); - given(tenantProfileConfigurationMock.getMaxRuleNodeDebugModeDurationMinutes(anyInt())).willReturn(maxRuleNodeDebugModeDurationMinutes); + private static long getUntilTime(int maxRuleNodeDebugModeDurationMinutes) { + return System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(maxRuleNodeDebugModeDurationMinutes); } - } diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index a09ca1d148..3220620dfd 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -71,7 +71,6 @@ import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.NumericFilterPredicate; import org.thingsboard.server.common.data.queue.Queue; -import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -164,7 +163,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { } private RuleChainId getEdgeRootRuleChainId() throws Exception { - return doGetTypedWithPageLink("/api/ruleChains?type={type}&", new TypeReference>() {}, + return doGetTypedWithPageLink("/api/ruleChains?type={type}&", new TypeReference>() { + }, new PageLink(100, 0, "Edge Root Rule Chain"), "EDGE") .getData().get(0).getId(); @@ -208,7 +208,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { protected void updateRootRuleChainMetadata() throws Exception { RuleChainId rootRuleChainId = getEdgeRootRuleChainId(); RuleChainMetaData rootRuleChainMetadata = doGet("/api/ruleChain/" + rootRuleChainId.getId().toString() + "/metadata", RuleChainMetaData.class); - rootRuleChainMetadata.getNodes().forEach(n -> n.setDebugStrategy(DebugStrategy.ALL_EVENTS)); + rootRuleChainMetadata.getNodes().forEach(n -> n.setDebugAll(true)); doPost("/api/ruleChain/metadata", rootRuleChainMetadata, RuleChainMetaData.class); } diff --git a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java index 2dc2bd42f3..224c21ffc5 100644 --- a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java @@ -23,7 +23,6 @@ import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.RuleChainId; -import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -229,7 +228,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { // update metadata for root rule chain edgeImitator.expectMessageAmount(1); - metaData.getNodes().forEach(n -> n.setDebugStrategy(DebugStrategy.ALL_EVENTS)); + metaData.getNodes().forEach(n -> n.setDebugAll(true)); doPost("/api/ruleChain/metadata", metaData, RuleChainMetaData.class); Assert.assertTrue(edgeImitator.waitForMessages()); ruleChainUpdateMsgOpt = edgeImitator.findMessageByType(RuleChainUpdateMsg.class); diff --git a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java index 3af4c012e0..0798ff8fdb 100644 --- a/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java @@ -43,7 +43,6 @@ import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; @@ -143,7 +142,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule ruleNode1.setName("Simple Rule Node 1"); ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode1.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode1.setDebugAll(true); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); configuration1.setFetchTo(TbMsgSource.METADATA); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); @@ -153,7 +152,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode2.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode2.setDebugAll(true); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); configuration2.setFetchTo(TbMsgSource.METADATA); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); @@ -249,7 +248,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule ruleNode1.setName("Simple Rule Node 1"); ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode1.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode1.setDebugAll(true); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); configuration1.setFetchTo(TbMsgSource.METADATA); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); @@ -258,7 +257,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule RuleNode ruleNode12 = new RuleNode(); ruleNode12.setName("Simple Rule Node 1"); ruleNode12.setType(org.thingsboard.rule.engine.flow.TbRuleChainInputNode.class.getName()); - ruleNode12.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode12.setDebugAll(true); TbRuleChainInputNodeConfiguration configuration12 = new TbRuleChainInputNodeConfiguration(); configuration12.setRuleChainId(secondaryRuleChain.getId().getId().toString()); ruleNode12.setConfiguration(JacksonUtil.valueToTree(configuration12)); @@ -283,7 +282,7 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode2.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode2.setDebugAll(true); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); configuration2.setFetchTo(TbMsgSource.METADATA); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); diff --git a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java index dd148748ca..efc817f6d2 100644 --- a/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java @@ -36,7 +36,6 @@ import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; @@ -98,7 +97,7 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac ruleNode.setName("Simple Rule Node"); ruleNode.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode.setDebugAll(true); TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration(); configuration.setFetchTo(TbMsgSource.METADATA); configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey")); diff --git a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java index 752624fa51..a3a2b55c76 100644 --- a/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java @@ -58,7 +58,6 @@ import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -463,7 +462,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { ruleNode1.setName("Simple Rule Node 1"); ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode1.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode1.setDebugAll(true); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1)); @@ -472,7 +471,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest { ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode2.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode2.setDebugAll(true); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java index 06bc670804..fdbf6b1e68 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java @@ -65,7 +65,6 @@ import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -472,7 +471,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { RuleNode ruleNode1 = new RuleNode(); ruleNode1.setName("Generator 1"); ruleNode1.setType(TbMsgGeneratorNode.class.getName()); - ruleNode1.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode1.setDebugAllUntil(System.currentTimeMillis()); TbMsgGeneratorNodeConfiguration configuration1 = new TbMsgGeneratorNodeConfiguration(); configuration1.setOriginatorType(originatorId.getEntityType()); configuration1.setOriginatorId(originatorId.getId().toString()); @@ -482,7 +481,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode2.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode2.setDebugAllUntil(System.currentTimeMillis()); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); @@ -511,7 +510,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { ruleNode1.setName("Simple Rule Node 1"); ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode1.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode1.setDebugAllUntil(System.currentTimeMillis()); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1)); @@ -520,7 +519,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest { ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode2.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode2.setDebugAllUntil(System.currentTimeMillis()); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index 21f6363f5f..194aaee8e0 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -65,7 +65,6 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -871,7 +870,7 @@ public class VersionControlTest extends AbstractControllerTest { RuleNode ruleNode1 = new RuleNode(); ruleNode1.setName("Generator 1"); ruleNode1.setType(TbMsgGeneratorNode.class.getName()); - ruleNode1.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode1.setDebugAll(true); TbMsgGeneratorNodeConfiguration configuration1 = new TbMsgGeneratorNodeConfiguration(); configuration1.setOriginatorType(originatorId.getEntityType()); configuration1.setOriginatorId(originatorId.getId().toString()); @@ -881,7 +880,7 @@ public class VersionControlTest extends AbstractControllerTest { ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode2.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode2.setDebugAll(true); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); @@ -909,7 +908,7 @@ public class VersionControlTest extends AbstractControllerTest { ruleNode1.setName("Simple Rule Node 1"); ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode1.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode1.setDebugAll(true); TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration(); configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1")); ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1)); @@ -918,7 +917,7 @@ public class VersionControlTest extends AbstractControllerTest { ruleNode2.setName("Simple Rule Node 2"); ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName()); ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); - ruleNode2.setDebugStrategy(DebugStrategy.ALL_EVENTS); + ruleNode2.setDebugAll(true); TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration(); configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2")); ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2)); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/DebugStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DebugStrategy.java deleted file mode 100644 index a065c9065f..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/DebugStrategy.java +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright © 2016-2024 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.rule; - -import lombok.Getter; -import org.thingsboard.server.common.data.msg.TbNodeConnectionType; - -import java.util.Set; -import java.util.concurrent.TimeUnit; - -@Getter -public enum DebugStrategy { - DISABLED(0, false), - ALL_EVENTS(1, true), - ALL_THEN_ONLY_FAILURE_EVENTS(2, true), - ONLY_FAILURE_EVENTS(3, false); - - private final int protoNumber; - private final boolean hasDuration; - - DebugStrategy(int protoNumber, boolean hasDuration) { - this.protoNumber = protoNumber; - this.hasDuration = hasDuration; - } - - public boolean shouldPersistDebugInput(long lastUpdateTs, long msgTs, int debugModeDurationMinutes) { - return isAllEventsStrategyAndMsgTsWithinDebugDuration(lastUpdateTs, msgTs, debugModeDurationMinutes); - } - - public boolean shouldPersistDebugOutputForAllEvents(long lastUpdateTs, long msgTs, int debugModeDurationMinutes) { - return this.isAllEventsStrategyAndMsgTsWithinDebugDuration(lastUpdateTs, msgTs, debugModeDurationMinutes); - } - - public boolean shouldPersistDebugForFailureEvent(Set nodeConnections) { - return isFailureStrategy() && nodeConnections.contains(TbNodeConnectionType.FAILURE); - } - - public boolean shouldPersistDebugForFailureEvent(String nodeConnection) { - return isFailureStrategy() && TbNodeConnectionType.FAILURE.equals(nodeConnection); - } - - private boolean isFailureStrategy() { - return DebugStrategy.ONLY_FAILURE_EVENTS.equals(this) || DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS.equals(this); - } - - private boolean isAllEventsStrategyAndMsgTsWithinDebugDuration(long lastUpdateTs, long msgTs, int debugModeDurationMinutes) { - return this.hasDuration && isMsgTsWithinDebugDuration(lastUpdateTs, msgTs, debugModeDurationMinutes); - } - - private boolean isMsgTsWithinDebugDuration(long lastUpdateTs, long msgCreationTs, int debugModeDurationMinutes) { - if (debugModeDurationMinutes <= 0) { - return true; - } - return msgCreationTs < lastUpdateTs + TimeUnit.MINUTES.toMillis(debugModeDurationMinutes); - } - -} 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 35601afe02..bbc244676c 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 @@ -47,10 +47,12 @@ public class RuleNode extends BaseDataWithAdditionalInfo implements @Length(fieldName = "name") @Schema(description = "User defined name of the rule node. Used on UI and for logging. ", example = "Process sensor reading") private String name; - @Schema(description = "Timestamp of the last rule node update.") - private long lastUpdateTs; - @Schema(description = "Debug strategy. ", example = "ALL_EVENTS") - private DebugStrategy debugStrategy; + @Schema(description = "Debug failures. ", example = "false") + private boolean debugFailures; + @Schema(description = "Debug All. Used as a trigger for updating debugAllUntil.", example = "false") + private boolean debugAll; + @Schema(description = "Timestamp of the end time for the processing debug events.") + private long debugAllUntil; @Schema(description = "Enable/disable singleton mode. ", example = "false") private boolean singletonMode; @Schema(description = "Queue name. ", example = "Main") @@ -77,8 +79,9 @@ public class RuleNode extends BaseDataWithAdditionalInfo implements this.ruleChainId = ruleNode.getRuleChainId(); this.type = ruleNode.getType(); this.name = ruleNode.getName(); - this.lastUpdateTs = ruleNode.getLastUpdateTs(); - this.debugStrategy = ruleNode.getDebugStrategy(); + this.debugFailures = ruleNode.isDebugFailures(); + this.debugAll = ruleNode.isDebugAll(); + this.debugAllUntil = ruleNode.getDebugAllUntil(); this.singletonMode = ruleNode.isSingletonMode(); this.setConfiguration(ruleNode.getConfiguration()); this.externalId = ruleNode.getExternalId(); @@ -89,10 +92,6 @@ public class RuleNode extends BaseDataWithAdditionalInfo implements return name; } - public DebugStrategy getDebugStrategy() { - return debugStrategy == null ? DebugStrategy.DISABLED : debugStrategy; - } - public JsonNode getConfiguration() { return BaseDataWithAdditionalInfo.getJson(() -> configuration, () -> configurationBytes); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNodeDebugUtil.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNodeDebugUtil.java new file mode 100644 index 0000000000..c6a3b495c8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNodeDebugUtil.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2024 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.rule; + +import org.thingsboard.server.common.data.msg.TbNodeConnectionType; + +import java.util.Set; + +public final class RuleNodeDebugUtil { + private RuleNodeDebugUtil() {} + + public static boolean isDebugAllAvailable(RuleNode ruleNode) { + return ruleNode.getDebugAllUntil() > System.currentTimeMillis(); + } + + public static boolean isDebugAvailable(RuleNode ruleNode, String nodeConnection) { + return isDebugAllAvailable(ruleNode) || ruleNode.isDebugFailures() && TbNodeConnectionType.FAILURE.equals(nodeConnection); + } + + public static boolean isDebugFailuresAvailable(RuleNode ruleNode, Set nodeConnections) { + return isDebugFailuresAvailable(ruleNode) && nodeConnections.contains(TbNodeConnectionType.FAILURE); + } + + public static boolean isDebugFailuresAvailable(RuleNode ruleNode, String nodeConnection) { + return isDebugFailuresAvailable(ruleNode) && TbNodeConnectionType.FAILURE.equals(nodeConnection); + } + + public static boolean isDebugFailuresAvailable(RuleNode ruleNode) { + return ruleNode.isDebugFailures() || isDebugAllAvailable(ruleNode); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index ff5c0ea426..1b36578a77 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -205,6 +205,9 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura @Override public int getMaxRuleNodeDebugModeDurationMinutes(int systemMaxRuleNodeDebugModeDurationMinutes) { - return Math.min(systemMaxRuleNodeDebugModeDurationMinutes, maxRuleNodeDebugDurationMinutes); + if (maxRuleNodeDebugDurationMinutes > 0) { + return Math.min(systemMaxRuleNodeDebugModeDurationMinutes, maxRuleNodeDebugDurationMinutes); + } + return systemMaxRuleNodeDebugModeDurationMinutes; } } diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index 167e160648..379ea3b77b 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -162,13 +162,6 @@ message RuleChainMetadataUpdateMsg { string entity = 8; } -enum DebugStrategy { - DISABLED = 0; - ALL_EVENTS = 1; - ALL_THEN_ONLY_FAILURE_EVENTS = 2; - ONLY_FAILURE_EVENTS = 3; -} - message RuleNodeProto { option deprecated = true; int64 idMSB = 1; @@ -180,8 +173,8 @@ message RuleNodeProto { string additionalInfo = 7; bool singletonMode = 8; int32 configurationVersion = 9; - int64 lastUpdateTs = 10; - DebugStrategy debugStrategy = 11; + bool debugFailures = 10; + int64 debugAllUntil = 11; } message NodeConnectionInfoProto { 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 4311bfa11d..fc987218cf 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 @@ -398,7 +398,8 @@ 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 DEBUG_STRATEGY = "debug_strategy"; + public static final String DEBUG_FAILURES = "debug_failures"; + public static final String DEBUG__ALL_UNTIL = "debug_all_until"; public static final String SINGLETON_MODE = "singleton_mode"; public static final String QUEUE_NAME = "queue_name"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java index 278fde0d6d..d1efb19061 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java @@ -19,14 +19,11 @@ import com.fasterxml.jackson.databind.JsonNode; import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; -import org.thingsboard.server.common.data.rule.DebugStrategy; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.BaseSqlEntity; @@ -61,12 +58,11 @@ public class RuleNodeEntity extends BaseSqlEntity { @Column(name = ModelConstants.ADDITIONAL_INFO_PROPERTY) private JsonNode additionalInfo; - @Column(name = ModelConstants.LAST_UPDATE_TS_COLUMN) - private long lastUpdateTs; + @Column(name = ModelConstants.DEBUG_FAILURES) + private boolean debugFailures; - @Enumerated(EnumType.STRING) - @Column(name = ModelConstants.DEBUG_STRATEGY) - private DebugStrategy debugStrategy; + @Column(name = ModelConstants.DEBUG__ALL_UNTIL) + private long debugAllUntil; @Column(name = ModelConstants.SINGLETON_MODE) private boolean singletonMode; @@ -90,8 +86,8 @@ public class RuleNodeEntity extends BaseSqlEntity { } this.type = ruleNode.getType(); this.name = ruleNode.getName(); - this.lastUpdateTs = ruleNode.getLastUpdateTs(); - this.debugStrategy = ruleNode.getDebugStrategy(); + this.debugFailures = ruleNode.isDebugFailures(); + this.debugAllUntil = ruleNode.getDebugAllUntil(); this.singletonMode = ruleNode.isSingletonMode(); this.queueName = ruleNode.getQueueName(); this.configurationVersion = ruleNode.getConfigurationVersion(); @@ -111,8 +107,8 @@ public class RuleNodeEntity extends BaseSqlEntity { } ruleNode.setType(type); ruleNode.setName(name); - ruleNode.setLastUpdateTs(lastUpdateTs); - ruleNode.setDebugStrategy(debugStrategy); + ruleNode.setDebugFailures(debugFailures); + ruleNode.setDebugAllUntil(debugAllUntil); ruleNode.setSingletonMode(singletonMode); ruleNode.setQueueName(queueName); ruleNode.setConfigurationVersion(configurationVersion); 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 f141ef627e..74375b1c78 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 @@ -24,6 +24,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.common.util.JacksonUtil; @@ -44,7 +45,6 @@ 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.DebugStrategy; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainConnectionInfo; @@ -66,6 +66,8 @@ import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.RuleChainDataValidator; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.util.TimeUtils; import java.util.ArrayList; import java.util.Collection; @@ -77,6 +79,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; @@ -110,6 +113,12 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Autowired private DataValidator ruleChainValidator; + @Autowired + private TbTenantProfileCache tbTenantProfileCache; + + @Value("${actors.rule.node.max_debug_mode_duration:60}") + private int maxRuleNodeDebugModeDurationMinutes; + @Override @Transactional public RuleChain saveRuleChain(RuleChain ruleChain) { @@ -215,11 +224,16 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } RuleChainId ruleChainId = ruleChain.getId(); if (nodes != null) { - long lastUpdateTs = System.currentTimeMillis(); + long now = System.currentTimeMillis(); for (RuleNode node : toAddOrUpdate) { node.setRuleChainId(ruleChainId); node = ruleNodeUpdater.apply(node); - node.setLastUpdateTs(lastUpdateTs); + + if (node.isDebugAll()) { + int debugDuration = tbTenantProfileCache.get(tenantId).getDefaultProfileConfiguration().getMaxRuleNodeDebugModeDurationMinutes(maxRuleNodeDebugModeDurationMinutes); + node.setDebugAllUntil(now + TimeUnit.MINUTES.toMillis(debugDuration)); + } + RuleChainDataValidator.validateRuleNode(node); RuleNode savedNode = ruleNodeDao.save(tenantId, node); relations.add(new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(), @@ -264,7 +278,6 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC layout.remove("description"); layout.remove("ruleChainNodeId"); targetNode.setAdditionalInfo(layout); - targetNode.setDebugStrategy(DebugStrategy.DISABLED); targetNode = ruleNodeDao.save(tenantId, targetNode); EntityRelation sourceRuleChainToRuleNode = new EntityRelation(); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index c5f459773a..9e766392fc 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -181,8 +181,8 @@ CREATE TABLE IF NOT EXISTS rule_node ( configuration varchar(10000000), type varchar(255), name varchar(255), - last_update_ts bigint NOT NULL, - debug_strategy varchar(32) DEFAULT 'DISABLED', + debug_failures boolean, + debug_all_until bigint NOT NULL, singleton_mode boolean, queue_name varchar(255), external_id uuid diff --git a/monitoring/src/main/resources/root_rule_chain.json b/monitoring/src/main/resources/root_rule_chain.json index 1da16c9b09..9e057490aa 100644 --- a/monitoring/src/main/resources/root_rule_chain.json +++ b/monitoring/src/main/resources/root_rule_chain.json @@ -20,7 +20,8 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", - "debugStrategy": "ALL_EVENTS", + "debugFailures": false, + "debugAll": true, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -35,7 +36,8 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Attributes", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "singletonMode": false, "configurationVersion": 1, "configuration": { @@ -53,7 +55,8 @@ }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "Message Type Switch", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -68,7 +71,8 @@ }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log RPC from Device", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -85,7 +89,8 @@ }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log Other", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -102,7 +107,8 @@ }, "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "name": "RPC Call Request", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -117,7 +123,8 @@ }, "type": "org.thingsboard.rule.engine.filter.TbOriginatorTypeFilterNode", "name": "Is Entity Group", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -134,7 +141,8 @@ }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeFilterNode", "name": "Post attributes or RPC request", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -152,7 +160,8 @@ }, "type": "org.thingsboard.rule.engine.transform.TbDuplicateMsgToGroupNode", "name": "Duplicate To Group Entities", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -169,7 +178,8 @@ }, "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", "name": "Device Profile Node", - "debugStrategy": "ALL_EVENTS", + "debugFailures": false, + "debugAll": true, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -186,7 +196,8 @@ }, "type": "org.thingsboard.rule.engine.filter.TbJsFilterNode", "name": "Test JS script", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -204,7 +215,8 @@ }, "type": "org.thingsboard.rule.engine.filter.TbJsFilterNode", "name": "Test TBEL script", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -222,7 +234,8 @@ }, "type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode", "name": "Add arrival timestamp", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -240,7 +253,8 @@ }, "type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode", "name": "Calculate additional latencies", - "debugStrategy": "ALL_EVENTS", + "debugFailures": false, + "debugAll": true, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -258,7 +272,8 @@ }, "type": "org.thingsboard.rule.engine.transform.TbChangeOriginatorNode", "name": "To latencies asset", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -287,7 +302,8 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", - "debugStrategy": "ALL_EVENTS", + "debugFailures": false, + "debugAll": true, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -303,7 +319,8 @@ }, "type": "org.thingsboard.rule.engine.filter.TbCheckMessageNode", "name": "Has testData", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -323,7 +340,8 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries with TTL", - "debugStrategy": "ALL_EVENTS", + "debugFailures": false, + "debugAll": true, "singletonMode": false, "configurationVersion": 0, "configuration": { diff --git a/msa/black-box-tests/src/test/resources/MqttRuleNodeTestMetadata.json b/msa/black-box-tests/src/test/resources/MqttRuleNodeTestMetadata.json index dc6a394a39..e6c93cffe1 100644 --- a/msa/black-box-tests/src/test/resources/MqttRuleNodeTestMetadata.json +++ b/msa/black-box-tests/src/test/resources/MqttRuleNodeTestMetadata.json @@ -9,7 +9,8 @@ }, "type": "org.thingsboard.rule.engine.mqtt.TbMqttNode", "name": "test mqtt", - "debugStrategy": "ALL_EVENTS", + "debugFailures": false, + "debugAll": true, "singletonMode": true, "queueName": "HighPriority", "configurationVersion": 0, @@ -36,7 +37,8 @@ }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "save timeseries", - "debugStrategy": "ALL_EVENTS", + "debugFailures": false, + "debugAll": true, "singletonMode": false, "configurationVersion": 0, "configuration": { @@ -54,7 +56,8 @@ }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "switch", - "debugStrategy": "DISABLED", + "debugFailures": false, + "debugAll": false, "singletonMode": false, "configurationVersion": 0, "configuration": { diff --git a/msa/black-box-tests/src/test/resources/RpcResponseRuleChainMetadata.json b/msa/black-box-tests/src/test/resources/RpcResponseRuleChainMetadata.json index 31eb3149f3..ddb9f8bec1 100644 --- a/msa/black-box-tests/src/test/resources/RpcResponseRuleChainMetadata.json +++ b/msa/black-box-tests/src/test/resources/RpcResponseRuleChainMetadata.json @@ -8,7 +8,8 @@ }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "msgTypeSwitch", - "debugStrategy": "ALL_EVENTS", + "debugFailures": false, + "debugAll": true, "configuration": { "version": 0 } @@ -20,7 +21,8 @@ }, "type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode", "name": "formResponse", - "debugStrategy": "ALL_EVENTS", + "debugFailures": false, + "debugAll": true, "configuration": { "jsScript": "if (msg.method == \"getResponse\") {\n return {msg: {\"response\": \"requestReceived\"}, metadata: metadata, msgType: msgType};\n}\n\nreturn {msg: msg, metadata: metadata, msgType: msgType};" } @@ -32,7 +34,8 @@ }, "type": "org.thingsboard.rule.engine.rpc.TbSendRPCReplyNode", "name": "rpcReply", - "debugStrategy": "ALL_EVENTS", + "debugFailures": false, + "debugAll": true, "configuration": { "requestIdMetaDataAttribute": "requestId" } From 7495861080350353d6bc66c6c7cde4f982d0b2a3 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 12 Nov 2024 09:46:20 +0100 Subject: [PATCH 22/77] added validation for the debugAllUntil --- .../thingsboard/server/dao/rule/BaseRuleChainService.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 74375b1c78..94eb1ad283 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 @@ -229,9 +229,13 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC node.setRuleChainId(ruleChainId); node = ruleNodeUpdater.apply(node); + int debugDuration = tbTenantProfileCache.get(tenantId).getDefaultProfileConfiguration().getMaxRuleNodeDebugModeDurationMinutes(maxRuleNodeDebugModeDurationMinutes); + long debugUntil = now + TimeUnit.MINUTES.toMillis(debugDuration); + if (node.isDebugAll()) { - int debugDuration = tbTenantProfileCache.get(tenantId).getDefaultProfileConfiguration().getMaxRuleNodeDebugModeDurationMinutes(maxRuleNodeDebugModeDurationMinutes); - node.setDebugAllUntil(now + TimeUnit.MINUTES.toMillis(debugDuration)); + node.setDebugAllUntil(debugUntil); + } else if (node.getDebugAllUntil() > debugUntil) { + throw new DataValidationException("Unable to update 'debugAllUntil' property. To reset the debug duration, please modify the 'debugAll' property instead."); } RuleChainDataValidator.validateRuleNode(node); From 78decec8da3893e3320cf529768508b821fa149b Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 12 Nov 2024 10:35:34 +0100 Subject: [PATCH 23/77] fixed default rule-chains creation --- .../org/thingsboard/server/service/install/InstallScripts.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index 115208bc47..75c0c0873c 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -202,7 +202,7 @@ public class InstallScripts { ruleChain = ruleChainService.saveRuleChain(ruleChain, false); ruleChainMetaData.setRuleChainId(ruleChain.getId()); - ruleChainService.saveRuleChainMetaData(TenantId.SYS_TENANT_ID, ruleChainMetaData, Function.identity(), false); + ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData, Function.identity(), false); return ruleChain; } From cf377b00830f485506416ee333da430796e7373d Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 12 Nov 2024 11:04:42 +0100 Subject: [PATCH 24/77] fixed circular references --- .../org/thingsboard/server/dao/rule/BaseRuleChainService.java | 2 ++ 1 file changed, 2 insertions(+) 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 94eb1ad283..52f28db5db 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 @@ -25,6 +25,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.common.util.JacksonUtil; @@ -114,6 +115,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC private DataValidator ruleChainValidator; @Autowired + @Lazy private TbTenantProfileCache tbTenantProfileCache; @Value("${actors.rule.node.max_debug_mode_duration:60}") From 9a1c92007d93e12a65f57ede8e9e9de96940ba55 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Tue, 12 Nov 2024 17:13:58 +0200 Subject: [PATCH 25/77] Changed aproach to debug config --- ...tml => debug-config-button.component.html} | 12 +- ...nt.ts => debug-config-button.component.ts} | 72 +++++------- .../debug-config-panel.component.html | 70 ++++++++++++ .../rulechain/debug-config-panel.component.ts | 105 ++++++++++++++++++ .../rulechain/debug-duration-left.pipe.ts | 11 +- .../debug-strategy-panel.component.html | 55 --------- .../debug-strategy-panel.component.ts | 97 ---------------- .../rule-node-details.component.html | 8 +- .../rulechain/rule-node-details.component.ts | 13 ++- .../rulechain/rulechain-page.component.ts | 24 ++-- .../home/pages/rulechain/rulechain.module.ts | 4 +- .../import-export/import-export.service.ts | 6 +- .../src/app/shared/models/rule-node.models.ts | 19 ++-- .../assets/locale/locale.constant-en_US.json | 4 +- 14 files changed, 258 insertions(+), 242 deletions(-) rename ui-ngx/src/app/modules/home/pages/rulechain/{debug-strategy-button.component.html => debug-config-button.component.html} (60%) rename ui-ngx/src/app/modules/home/pages/rulechain/{debug-strategy-button.component.ts => debug-config-button.component.ts} (53%) create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/debug-config-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/debug-config-panel.component.ts delete mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.html delete mode 100644 ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.ts diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/debug-config-button.component.html similarity index 60% rename from ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.html rename to ui-ngx/src/app/modules/home/pages/rulechain/debug-config-button.component.html index ea94a99c74..d5518b71f1 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-config-button.component.html @@ -19,14 +19,12 @@ class="tb-rounded-btn flex-1 w-36" color="primary" #matButton - [class.active]="debugStrategyFormControl.value !== DebugStrategy.DISABLED && !disabled" + [class.active]="(isDebugAllActive() || debugFailures) && !disabled" [disabled]="disabled" (click)="openDebugStrategyPanel($event, matButton)"> bug_report - - common.disabled - debug-strategy.all - {{ lastUpdateTs | debugDurationLeft : maxRuleNodeDebugDurationMinutes }} - debug-strategy.failures - + common.disabled + debug-config.all + {{ debugAllUntil | debugDurationLeft }} + debug-config.failures diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/debug-config-button.component.ts similarity index 53% rename from ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.ts rename to ui-ngx/src/app/modules/home/pages/rulechain/debug-config-button.component.ts index 3422949b5a..ff05942df4 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-button.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-config-button.component.ts @@ -17,38 +17,30 @@ import { Component, Input, - forwardRef, Renderer2, ViewContainerRef, DestroyRef, ChangeDetectionStrategy, - ChangeDetectorRef + ChangeDetectorRef, + EventEmitter, + Output } from '@angular/core'; -import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, UntypedFormBuilder } from '@angular/forms'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; -import { DebugStrategy } from '@shared/models/rule-node.models'; import { DebugDurationLeftPipe } from '@home/pages/rulechain/debug-duration-left.pipe'; -import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { TbPopoverService } from '@shared/components/popover.service'; import { MatButton } from '@angular/material/button'; -import { DebugStrategyPanelComponent } from '@home/pages/rulechain/debug-strategy-panel.component'; +import { DebugConfigPanelComponent } from '@home/pages/rulechain/debug-config-panel.component'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { interval } from 'rxjs'; -import { MINUTE } from '@shared/models/time/time.models'; +import { SECOND } from '@shared/models/time/time.models'; +import { RuleNodeDebugConfig } from '@shared/models/rule-node.models'; @Component({ - selector: 'tb-debug-strategy-button', - templateUrl: './debug-strategy-button.component.html', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => DebugStrategyButtonComponent), - multi: true - } - ], + selector: 'tb-debug-config-button', + templateUrl: './debug-config-button.component.html', standalone: true, imports: [ CommonModule, @@ -57,29 +49,23 @@ import { MINUTE } from '@shared/models/time/time.models'; ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class DebugStrategyButtonComponent implements ControlValueAccessor { - - @Input() lastUpdateTs: number; +export class DebugConfigButtonComponent { - debugStrategyFormControl: FormControl; - disabled = false; + @Input() debugFailures = false; + @Input() debugAll = false; + @Input() debugAllUntil = 0; + @Input() disabled = false; - private onChange: (debugStrategy: DebugStrategy) => void; - - readonly maxRuleNodeDebugDurationMinutes = getCurrentAuthState(this.store).maxRuleNodeDebugDurationMinutes; - readonly DebugStrategy = DebugStrategy; + @Output() onDebugConfigChanged = new EventEmitter() constructor(protected store: Store, - private fb: UntypedFormBuilder, private popoverService: TbPopoverService, private renderer: Renderer2, private viewContainerRef: ViewContainerRef, private destroyRef: DestroyRef, private cdr: ChangeDetectorRef ) { - this.debugStrategyFormControl = this.fb.control(DebugStrategy.DISABLED); - this.debugStrategyFormControl.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.onChange(value)); - interval(0.5 * MINUTE) + interval(SECOND) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => this.cdr.markForCheck()); } @@ -89,36 +75,28 @@ export class DebugStrategyButtonComponent implements ControlValueAccessor { $event.stopPropagation(); } const trigger = matButton._elementRef.nativeElement; - const debugStrategy = this.debugStrategyFormControl.value; if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { const debugStrategyPopover = this.popoverService.displayPopover(trigger, this.renderer, - this.viewContainerRef, DebugStrategyPanelComponent, 'bottom', true, null, - { debugStrategy }, + this.viewContainerRef, DebugConfigPanelComponent, 'bottom', true, null, + { + debugFailures: this.debugFailures, + debugAll: this.debugAll, + debugAllUntil: this.debugAllUntil, + }, {}, {}, {}, true); debugStrategyPopover.tbComponentRef.instance.popover = debugStrategyPopover; - debugStrategyPopover.tbComponentRef.instance.onStrategyApplied.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((strategy: DebugStrategy) => { - this.debugStrategyFormControl.patchValue(strategy); + debugStrategyPopover.tbComponentRef.instance.onConfigApplied.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((config: RuleNodeDebugConfig) => { + this.onDebugConfigChanged.emit(config); this.cdr.markForCheck(); debugStrategyPopover.hide(); }); } } - registerOnChange(onChange: (debugStrategy: DebugStrategy) => void): void { - this.onChange = onChange; - } - - registerOnTouched(_: () => {}): void {} - - setDisabledState(isDisabled: boolean): void { - this.disabled = isDisabled; - } - - writeValue(value: DebugStrategy): void { - this.debugStrategyFormControl.patchValue(value, { emitEvent: false }); - this.cdr.markForCheck(); + isDebugAllActive(): boolean { + return this.debugAllUntil > new Date().getTime(); } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-config-panel.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/debug-config-panel.component.html new file mode 100644 index 0000000000..b7d2f44d8d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-config-panel.component.html @@ -0,0 +1,70 @@ + +
+
debug-config.label
+
+
+ + {{ 'debug-config.hint.main-limited' | translate: { msg: maxMessagesCount, sec: maxTimeFrameSec } }} + + {{ 'debug-config.hint.main' | translate }} +
+
+
+ + + {{ 'debug-config.on-failure' | translate }} + + +
+ + + {{ 'debug-config.all-messages' | translate }} {{ '( ' }} + {{ debugAllUntil | debugDurationLeft }} + + {{ maxRuleNodeDebugDurationMinutes + ' ' }} {{ ('debug-config.min' | translate) }} + + {{ ' )' }} + + + +
+
+
+ + +
+
+ diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-config-panel.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/debug-config-panel.component.ts new file mode 100644 index 0000000000..a2f8091cfe --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-config-panel.component.ts @@ -0,0 +1,105 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, DestroyRef, EventEmitter, Input, OnInit } from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { FormControl, UntypedFormBuilder } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { RuleNodeDebugConfig } from '@shared/models/rule-node.models'; +import { MINUTE, SECOND } from '@shared/models/time/time.models'; +import { DebugDurationLeftPipe } from '@home/pages/rulechain/debug-duration-left.pipe'; +import { interval } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +@Component({ + selector: 'tb-debug-config-panel', + templateUrl: './debug-config-panel.component.html', + standalone: true, + imports: [ + SharedModule, + CommonModule, + DebugDurationLeftPipe + ] +}) +export class DebugConfigPanelComponent extends PageComponent implements OnInit { + + @Input() popover: TbPopoverComponent; + @Input() debugFailures = false; + @Input() debugAll = false; + @Input() debugAllUntil = 0; + + onFailuresControl: FormControl; + debugAllControl: FormControl; + + onConfigApplied = new EventEmitter() + + readonly maxRuleNodeDebugDurationMinutes = getCurrentAuthState(this.store).maxRuleNodeDebugDurationMinutes; + readonly ruleChainDebugPerTenantLimitsConfiguration = getCurrentAuthState(this.store).ruleChainDebugPerTenantLimitsConfiguration; + readonly maxMessagesCount = this.ruleChainDebugPerTenantLimitsConfiguration?.split(':')[0]; + readonly maxTimeFrameSec = this.ruleChainDebugPerTenantLimitsConfiguration?.split(':')[1]; + + constructor(private fb: UntypedFormBuilder, + private destroyRef: DestroyRef, + private cdr: ChangeDetectorRef, + protected store: Store) { + super(store); + + this.onFailuresControl = this.fb.control(false); + this.debugAllControl = this.fb.control(false); + interval(SECOND) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.debugAllControl.patchValue(this.isDebugAllOn(), { emitEvent: false }); + this.cdr.markForCheck(); + }); + this.debugAllControl.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => { + this.debugAllUntil = value? new Date().getTime() + this.maxRuleNodeDebugDurationMinutes * MINUTE : 0; + this.debugAll = value; + this.cdr.markForCheck(); + }); + } + + ngOnInit(): void { + this.debugAllControl.patchValue(this.isDebugAllOn(), { emitEvent: false }); + this.onFailuresControl.patchValue(this.debugFailures); + } + + onCancel() { + this.popover?.hide(); + } + + onApply(): void { + this.onConfigApplied.emit({ + debugAll: this.debugAll, + debugFailures: this.onFailuresControl.value, + debugAllUntil: this.debugAllUntil + }); + } + + onReset(): void { + this.debugAll = true; + this.debugAllUntil = new Date().getTime() + this.maxRuleNodeDebugDurationMinutes * MINUTE; + } + + isDebugAllOn(): boolean { + return this.debugAllUntil > new Date().getTime(); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts b/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts index d6615da476..bb23b20959 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/debug-duration-left.pipe.ts @@ -29,15 +29,8 @@ export class DebugDurationLeftPipe implements PipeTransform { constructor(private translate: TranslateService, private millisecondsToTimeString: MillisecondsToTimeStringPipe) { } - transform(lastUpdateTs: number, maxRuleNodeDebugDurationMinutes: number): string { - const time = this.millisecondsToTimeString.transform(this.getDebugTime(lastUpdateTs, maxRuleNodeDebugDurationMinutes), true, true); + transform(debugAllUntil: number): string { + const time = this.millisecondsToTimeString.transform((debugAllUntil - new Date().getTime()), true, true); return `${time} ` + this.translate.instant('common.left'); } - - private getDebugTime(lastUpdateTs: number, maxRuleNodeDebugDurationMinutes: number): number { - const maxDuration = maxRuleNodeDebugDurationMinutes * MINUTE; - const updateDuration = lastUpdateTs ? new Date().getTime() - lastUpdateTs : 0; - const leftTime = maxDuration - updateDuration; - return leftTime > 0 ? leftTime : 0; - } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.html deleted file mode 100644 index 0edf83096a..0000000000 --- a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.html +++ /dev/null @@ -1,55 +0,0 @@ - -
-
debug-strategy.label
-
-
- - {{ 'debug-strategy.hint.main-limited' | translate: { msg: maxMessagesCount, sec: maxTimeFrameSec } }} - - {{ 'debug-strategy.hint.main' | translate }} -
-
-
- - - {{ 'debug-strategy.on-failure' | translate }} - - - - - {{ 'debug-strategy.all-messages' | translate }} {{ ' (' + maxRuleNodeDebugDurationMinutes + ' ' }} {{ ('debug-strategy.min' | translate) + ')'}} - - -
-
- - -
-
- diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.ts deleted file mode 100644 index c24e8331d7..0000000000 --- a/ui-ngx/src/app/modules/home/pages/rulechain/debug-strategy-panel.component.ts +++ /dev/null @@ -1,97 +0,0 @@ -/// -/// Copyright © 2016-2024 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { Component, EventEmitter, Input, OnInit } from '@angular/core'; -import { PageComponent } from '@shared/components/page.component'; -import { TbPopoverComponent } from '@shared/components/popover.component'; -import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { CommonModule } from '@angular/common'; -import { SharedModule } from '@shared/shared.module'; -import { getCurrentAuthState } from '@core/auth/auth.selectors'; -import { DebugStrategy } from '@shared/models/rule-node.models'; - -@Component({ - selector: 'tb-debug-strategy-panel', - templateUrl: './debug-strategy-panel.component.html', - standalone: true, - imports: [ - SharedModule, - CommonModule - ] -}) -export class DebugStrategyPanelComponent extends PageComponent implements OnInit { - - @Input() popover: TbPopoverComponent; - @Input() debugStrategy: DebugStrategy; - - debugStrategyFormGroup: UntypedFormGroup; - - onStrategyApplied = new EventEmitter() - - readonly maxRuleNodeDebugDurationMinutes = getCurrentAuthState(this.store).maxRuleNodeDebugDurationMinutes; - readonly ruleChainDebugPerTenantLimitsConfiguration = getCurrentAuthState(this.store).ruleChainDebugPerTenantLimitsConfiguration; - readonly maxMessagesCount = this.ruleChainDebugPerTenantLimitsConfiguration?.split(':')[0]; - readonly maxTimeFrameSec = this.ruleChainDebugPerTenantLimitsConfiguration?.split(':')[1]; - - constructor(private fb: UntypedFormBuilder, - protected store: Store) { - super(store); - - this.debugStrategyFormGroup = this.fb.group({ - allMessages: [false], - onFailure: [false] - }); - } - - ngOnInit(): void { - this.updatePanelStrategy(); - } - - onCancel() { - this.popover?.hide(); - } - - onApply(): void { - const allMessages = this.debugStrategyFormGroup.get('allMessages').value; - const onFailure = this.debugStrategyFormGroup.get('onFailure').value; - if (allMessages && onFailure) { - this.onStrategyApplied.emit(DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS); - } else if (allMessages) { - this.onStrategyApplied.emit(DebugStrategy.ALL_EVENTS); - } else if (onFailure) { - this.onStrategyApplied.emit(DebugStrategy.ONLY_FAILURE_EVENTS); - } else { - this.onStrategyApplied.emit(DebugStrategy.DISABLED); - } - } - - private updatePanelStrategy(): void { - switch (this.debugStrategy) { - case DebugStrategy.ALL_THEN_ONLY_FAILURE_EVENTS: - this.debugStrategyFormGroup.get('allMessages').patchValue(true, { emitEvent: false }); - this.debugStrategyFormGroup.get('onFailure').patchValue(true, { emitEvent: false }); - break; - case DebugStrategy.ONLY_FAILURE_EVENTS: - this.debugStrategyFormGroup.get('onFailure').patchValue(true, { emitEvent: false }); - break; - case DebugStrategy.ALL_EVENTS: - this.debugStrategyFormGroup.get('allMessages').patchValue(true, { emitEvent: false }); - break; - } - } -} 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 c21cec6dd4..35415dec57 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 @@ -35,7 +35,13 @@
- + diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-config-button.component.ts b/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.ts similarity index 74% rename from ui-ngx/src/app/modules/home/pages/rulechain/debug-config-button.component.ts rename to ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.ts index ff05942df4..ad64833ace 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/debug-config-button.component.ts +++ b/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.ts @@ -21,22 +21,20 @@ import { ViewContainerRef, DestroyRef, ChangeDetectionStrategy, - ChangeDetectorRef, EventEmitter, Output } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; -import { DebugDurationLeftPipe } from '@home/pages/rulechain/debug-duration-left.pipe'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; +import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { TbPopoverService } from '@shared/components/popover.service'; import { MatButton } from '@angular/material/button'; -import { DebugConfigPanelComponent } from '@home/pages/rulechain/debug-config-panel.component'; +import { DebugConfigPanelComponent } from './debug-config-panel.component'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { interval } from 'rxjs'; +import { timer } from 'rxjs'; import { SECOND } from '@shared/models/time/time.models'; -import { RuleNodeDebugConfig } from '@shared/models/rule-node.models'; +import { HasDebugConfig } from '@shared/models/entity.models'; +import { map } from 'rxjs/operators'; @Component({ selector: 'tb-debug-config-button', @@ -45,7 +43,7 @@ import { RuleNodeDebugConfig } from '@shared/models/rule-node.models'; imports: [ CommonModule, SharedModule, - DebugDurationLeftPipe, + DurationLeftPipe, ], changeDetection: ChangeDetectionStrategy.OnPush }) @@ -55,20 +53,18 @@ export class DebugConfigButtonComponent { @Input() debugAll = false; @Input() debugAllUntil = 0; @Input() disabled = false; + @Input() maxRuleNodeDebugDurationMinutes: number; + @Input() ruleChainDebugPerTenantLimitsConfiguration: string; - @Output() onDebugConfigChanged = new EventEmitter() + @Output() onDebugConfigChanged = new EventEmitter(); - constructor(protected store: Store, - private popoverService: TbPopoverService, + isDebugAllActive$ = timer(0, SECOND).pipe(map(() => this.debugAllUntil > new Date().getTime())); + + constructor(private popoverService: TbPopoverService, private renderer: Renderer2, private viewContainerRef: ViewContainerRef, private destroyRef: DestroyRef, - private cdr: ChangeDetectorRef - ) { - interval(SECOND) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => this.cdr.markForCheck()); - } + ) {} openDebugStrategyPanel($event: Event, matButton: MatButton): void { if ($event) { @@ -84,19 +80,16 @@ export class DebugConfigButtonComponent { debugFailures: this.debugFailures, debugAll: this.debugAll, debugAllUntil: this.debugAllUntil, + maxRuleNodeDebugDurationMinutes: this.maxRuleNodeDebugDurationMinutes, + ruleChainDebugPerTenantLimitsConfiguration: this.ruleChainDebugPerTenantLimitsConfiguration }, {}, {}, {}, true); debugStrategyPopover.tbComponentRef.instance.popover = debugStrategyPopover; - debugStrategyPopover.tbComponentRef.instance.onConfigApplied.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((config: RuleNodeDebugConfig) => { + debugStrategyPopover.tbComponentRef.instance.onConfigApplied.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((config: HasDebugConfig) => { this.onDebugConfigChanged.emit(config); - this.cdr.markForCheck(); debugStrategyPopover.hide(); }); } } - - isDebugAllActive(): boolean { - return this.debugAllUntil > new Date().getTime(); - } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/debug-config-panel.component.html b/ui-ngx/src/app/modules/home/components/debug-config/debug-config-panel.component.html similarity index 75% rename from ui-ngx/src/app/modules/home/pages/rulechain/debug-config-panel.component.html rename to ui-ngx/src/app/modules/home/components/debug-config/debug-config-panel.component.html index b7d2f44d8d..b463882110 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/debug-config-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/debug-config/debug-config-panel.component.html @@ -27,23 +27,18 @@
- +
{{ 'debug-config.on-failure' | translate }} - +
- - {{ 'debug-config.all-messages' | translate }} {{ '( ' }} - {{ debugAllUntil | debugDurationLeft }} - - {{ maxRuleNodeDebugDurationMinutes + ' ' }} {{ ('debug-config.min' | translate) }} - - {{ ' )' }} - +
+ {{ 'debug-config.all-messages' | translate: { time: (isDebugAllActive$ | async) ? (debugAllUntil | durationLeft) : ('debug-config.min' | translate: { number: maxRuleNodeDebugDurationMinutes }) } }} +
-
+
+ + tenant-profile.maximum-debug-duration-min + + + {{ 'tenant-profile.maximum-debug-duration-min-range' | translate}} + + + {{ 'tenant-profile.maximum-debug-duration-min-required' | translate}} + + + +
+
@@ -147,22 +163,6 @@
-
- - tenant-profile.maximum-rule-node-debug-duration-min - - - {{ 'tenant-profile.maximum-rule-node-debug-duration-min-range' | translate}} - - - {{ 'tenant-profile.maximum-rule-node-debug-duration-min-required' | translate}} - - - -
-
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 7aca5ed0e2..f9f6fc190e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4308,9 +4308,9 @@ "maximum-ota-packages-sum-data-size": "Maximum total size of OTA package files (bytes)", "maximum-ota-package-sum-data-size-required": "Maximum total size of OTA package files is required.", "maximum-ota-package-sum-data-size-range": "Maximum total size of OTA package files can't be negative", - "maximum-rule-node-debug-duration-min": "Maximum rule node debug duration (min)", - "maximum-rule-node-debug-duration-min-required": "Maximum rule node debug duration is required.", - "maximum-rule-node-debug-duration-min-range": "Maximum rule node debug duration can't be negative", + "maximum-debug-duration-min": "Maximum debug duration (min)", + "maximum-debug-duration-min-required": "Maximum debug duration is required.", + "maximum-debug-duration-min-range": "Maximum debug duration can't be negative", "rest-requests-for-tenant": "REST requests for tenant", "transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages", "transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points", From d1e08bf64bd0d77409dfc424c5ed6efe6d822c0a Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 13 Nov 2024 15:47:07 +0200 Subject: [PATCH 35/77] refactoring --- ui-ngx/src/app/shared/models/rule-node.models.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts index db993664a8..1a6aa40e86 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -331,7 +331,7 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor { configurationDescriptor?: RuleNodeConfigurationDescriptor; } -export interface FcRuleNodeType extends FcNode { +export interface FcRuleNodeType extends FcNode, HasDebugConfig { component?: RuleNodeComponentDescriptor; singletonMode?: boolean; queueName?: string; From a70d40534746b0750440113071f206acffeac516 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 13 Nov 2024 15:56:53 +0200 Subject: [PATCH 36/77] refactoring --- .../default-tenant-profile-configuration.component.html | 4 ++-- ui-ngx/src/app/shared/models/entity.models.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index 21d2fe49af..dd095117d7 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -83,10 +83,10 @@ formControlName="maxDebugModeDurationMinutes" type="number"> - {{ 'tenant-profile.maximum-debug-duration-min-range' | translate}} + {{ 'tenant-profile.maximum-debug-duration-min-range' | translate }} - {{ 'tenant-profile.maximum-debug-duration-min-required' | translate}} + {{ 'tenant-profile.maximum-debug-duration-min-required' | translate }} diff --git a/ui-ngx/src/app/shared/models/entity.models.ts b/ui-ngx/src/app/shared/models/entity.models.ts index 62270a0240..cb209c3352 100644 --- a/ui-ngx/src/app/shared/models/entity.models.ts +++ b/ui-ngx/src/app/shared/models/entity.models.ts @@ -193,7 +193,7 @@ export interface HasVersion { } export interface HasDebugConfig { - debugAll: boolean; - debugFailures: boolean; - debugAllUntil: number; + debugAll?: boolean; + debugFailures?: boolean; + debugAllUntil?: number; } From 075718e9563de59095008227524c47642de7fef9 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Wed, 13 Nov 2024 17:00:44 +0200 Subject: [PATCH 37/77] added mini btn mode --- ui-ngx/src/app/core/auth/auth.reducer.ts | 2 +- .../debug-config-button.component.html | 14 +++++++++++++ .../debug-config-button.component.scss | 20 +++++++++++++++++++ .../debug-config-button.component.ts | 2 ++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.scss diff --git a/ui-ngx/src/app/core/auth/auth.reducer.ts b/ui-ngx/src/app/core/auth/auth.reducer.ts index 072f3f62a5..20674adf8d 100644 --- a/ui-ngx/src/app/core/auth/auth.reducer.ts +++ b/ui-ngx/src/app/core/auth/auth.reducer.ts @@ -31,8 +31,8 @@ const emptyUserAuthState: AuthPayload = { persistDeviceStateToTelemetry: false, mobileQrEnabled: false, maxResourceSize: 0, - userSettings: initialUserSettings, maxDebugModeDurationMinutes: 0, + userSettings: initialUserSettings }; export const initialState: AuthState = { diff --git a/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.html b/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.html index 59565281b3..d0905d968d 100644 --- a/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.html +++ b/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.html @@ -16,6 +16,7 @@ --> + + + diff --git a/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.scss b/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.scss new file mode 100644 index 0000000000..06fcba27be --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.scss @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2024 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. + */ +:host { + .mini-debug-btn { + color: rgba(0, 0, 0, 0.6); + } +} diff --git a/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.ts b/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.ts index 154bacf69f..576f926a06 100644 --- a/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.ts +++ b/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.ts @@ -42,6 +42,7 @@ import { Store } from '@ngrx/store'; @Component({ selector: 'tb-debug-config-button', templateUrl: './debug-config-button.component.html', + styleUrls: ['./debug-config-button.component.scss'], standalone: true, imports: [ CommonModule, @@ -56,6 +57,7 @@ export class DebugConfigButtonComponent { @Input() debugAll = false; @Input() debugAllUntil = 0; @Input() disabled = false; + @Input() minifyMode = false; @Input() debugLimitsConfiguration: string; @Output() onDebugConfigChanged = new EventEmitter(); From ed6e3f215408b9a40fc3a01e6f86722dfb8624e8 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 15 Nov 2024 12:19:49 +0200 Subject: [PATCH 38/77] Added static time on debugAll=true --- .../debug-config/debug-config-button.component.html | 4 +++- .../components/debug-config/debug-config-panel.component.html | 4 ++-- .../components/debug-config/debug-config-panel.component.ts | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.html b/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.html index d0905d968d..bdaf2de487 100644 --- a/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.html +++ b/ui-ngx/src/app/modules/home/components/debug-config/debug-config-button.component.html @@ -26,7 +26,9 @@ bug_report common.disabled debug-config.all - {{ debugAllUntil | durationLeft }} + + {{ !debugAll ? (debugAllUntil | durationLeft) : ('debug-config.min' | translate: { number: maxDebugModeDurationMinutes }) }} + debug-config.failures diff --git a/ui-ngx/src/app/modules/home/components/debug-config/debug-config-panel.component.html b/ui-ngx/src/app/modules/home/components/debug-config/debug-config-panel.component.html index 2e3eb94648..379ad579eb 100644 --- a/ui-ngx/src/app/modules/home/components/debug-config/debug-config-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/debug-config/debug-config-panel.component.html @@ -34,10 +34,10 @@
- {{ 'debug-config.all-messages' | translate: { time: (isDebugAllActive$ | async) ? (debugAllUntil | durationLeft) : ('debug-config.min' | translate: { number: maxDebugModeDurationMinutes }) } }} + {{ 'debug-config.all-messages' | translate: { time: (isDebugAllActive$ | async) && !debugAll ? (debugAllUntil | durationLeft) : ('debug-config.min' | translate: { number: maxDebugModeDurationMinutes }) } }}
-
-
+
admin.jwt.expiration-time + min="60"/> + {{ 'admin.jwt.expiration-time-required' | translate }} - - {{ 'admin.jwt.expiration-time-pattern' | translate }} + + {{ 'admin.jwt.expiration-time-max' | translate }} {{ 'admin.jwt.expiration-time-min' | translate }} - + admin.jwt.refresh-expiration-time + min="900"/> + {{ 'admin.jwt.refresh-expiration-time-required' | translate }} - - {{ 'admin.jwt.refresh-expiration-time-pattern' | translate }} + + {{ 'admin.jwt.refresh-expiration-time-max' | translate }} {{ 'admin.jwt.refresh-expiration-time-min' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts index 712e2bb358..01eff581a5 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts @@ -99,8 +99,8 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi this.jwtSecuritySettingsFormGroup = this.fb.group({ tokenIssuer: ['', Validators.required], tokenSigningKey: ['', [Validators.required, this.base64Format]], - tokenExpirationTime: [0, [Validators.required, Validators.pattern('[0-9]*'), Validators.min(60)]], - refreshTokenExpTime: [0, [Validators.required, Validators.pattern('[0-9]*'), Validators.min(900)]] + tokenExpirationTime: [0, [Validators.required, Validators.min(60), Validators.max(2147483647)]], + refreshTokenExpTime: [0, [Validators.required, Validators.min(900), Validators.max(2147483647)]] }, {validators: this.refreshTokenTimeGreatTokenTime.bind(this)}); this.jwtSecuritySettingsFormGroup.get('tokenExpirationTime').valueChanges.subscribe( () => this.jwtSecuritySettingsFormGroup.get('refreshTokenExpTime').updateValueAndValidity({onlySelf: true}) diff --git a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json index 0afd671082..a00efd798e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json +++ b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json @@ -488,11 +488,9 @@ "signings-key-base64": "يجب أن يكون مفتاح التوقيع بتنسيق base64.", "expiration-time": "وقت انتهاء صلاحية الرمز (ثانية)", "expiration-time-required": "وقت انتهاء صلاحية الرمز مطلوب.", - "expiration-time-pattern": "يجب أن يكون وقت انتهاء صلاحية الرمز عددًا صحيحًا موجبًا.", "expiration-time-min": "الحد الأدنى للوقت هو 60 ثانية (1 دقيقة).", "refresh-expiration-time": "وقت انتهاء صلاحية رمز التحديث (ثانية)", "refresh-expiration-time-required": "وقت انتهاء صلاحية رمز التحديث مطلوب.", - "refresh-expiration-time-pattern": "يجب أن يكون وقت انتهاء صلاحية رمز التحديث عددًا صحيحًا موجبًا.", "refresh-expiration-time-min": "الحد الأدنى للوقت هو 900 ثانية (15 دقيقة).", "refresh-expiration-time-less-token": "يجب أن يكون وقت رمز التحديث أكبر من وقت الرمز.", "generate-key": "توليد المفتاح", diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 405f4ae9d3..6f288f3db5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -524,11 +524,11 @@ "signings-key-base64": "Signing key must be base64 format.", "expiration-time": "Token expiration time (sec)", "expiration-time-required": "Token expiration time is required.", - "expiration-time-pattern": "Token expiration time be a positive integer.", + "expiration-time-max": "Maximal time is 2147483647 seconds (68 years).", "expiration-time-min": "Minimum time is 60 seconds (1 minute).", "refresh-expiration-time": "Refresh token expiration time (sec)", "refresh-expiration-time-required": "Refresh token expiration time is required.", - "refresh-expiration-time-pattern": "Refresh token expiration time be a positive integer.", + "refresh-expiration-time-max": "Maximal time is 2147483647 seconds (68 years).", "refresh-expiration-time-min": "Minimum time is 900 seconds (15 minute).", "refresh-expiration-time-less-token": "Refresh token time must be greater token time.", "generate-key": "Generate key", diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index f653e7179a..61b708222b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -437,11 +437,9 @@ "signings-key-base64": "La clave de firma debe estar en formato base64.", "expiration-time": "Caducidad del token (en segundos)", "expiration-time-required": "Se requiere caducidad del token.", - "expiration-time-pattern": "La caducidad debe ser un número entero positivo.", "expiration-time-min": "El tiempo mínimo debe ser al menos de 60 segundos (1 minuto).", "refresh-expiration-time": "Caducidad del token de actualización (en segundos)", "refresh-expiration-time-required": "Se requiere especificar caducidad del token de actualización.", - "refresh-expiration-time-pattern": "La caducidad debe ser un número entero positivo.", "refresh-expiration-time-min": "El tiempo mínimo es de 900 segundos (15 minutos).", "refresh-expiration-time-less-token": "El tiempo de actualización debe ser mayor al de caducidad.", "generate-key": "Generar clave", diff --git a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json index cb3a96f989..11c737bab4 100644 --- a/ui-ngx/src/assets/locale/locale.constant-lt_LT.json +++ b/ui-ngx/src/assets/locale/locale.constant-lt_LT.json @@ -463,11 +463,9 @@ "signings-key-base64": "Signing key must be base64 format.", "expiration-time": "Token expiration time (sec)", "expiration-time-required": "Token expiration time is required.", - "expiration-time-pattern": "Token expiration time be a positive integer.", "expiration-time-min": "Minimum time is 60 seconds (1 minute).", "refresh-expiration-time": "Refresh token expiration time (sec)", "refresh-expiration-time-required": "Refresh token expiration time is required.", - "refresh-expiration-time-pattern": "Refresh token expiration time be a positive integer.", "refresh-expiration-time-min": "Minimum time is 900 seconds (15 minute).", "refresh-expiration-time-less-token": "Refresh token time must be greater token time.", "generate-key": "Generate key", diff --git a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json index c16c9c5e40..85e75092b6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json +++ b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json @@ -431,11 +431,9 @@ "signings-key-base64": "De ondertekeningssleutel moet de base64-indeling hebben.", "expiration-time": "Vervaltijd token (sec)", "expiration-time-required": "De vervaltijd van het token is vereist.", - "expiration-time-pattern": "De vervaltijd van het token is een positief geheel getal.", "expiration-time-min": "De minimale tijd is 60 seconden (1 minuut).", "refresh-expiration-time": "Vervaltijd token vernieuwen (sec)", "refresh-expiration-time-required": "De vervaltijd van het vernieuwingstoken is vereist.", - "refresh-expiration-time-pattern": "De vervaltijd van het token vernieuwen is een positief geheel getal.", "refresh-expiration-time-min": "De minimale tijd is 900 seconden (15 minuten).", "refresh-expiration-time-less-token": "De tokentijd voor vernieuwen moet een grotere tokentijd zijn.", "generate-key": "Sleutel genereren", diff --git a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json index ac27313248..af95c71be0 100644 --- a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json +++ b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json @@ -463,11 +463,9 @@ "signings-key-base64": "Klucz podpisujący musi być w formacie base64.", "expiration-time": "Czas ważności tokena (s)", "expiration-time-required": "Czas ważności tokena jest wymagany.", - "expiration-time-pattern": "Czas ważności tokena musi być dodatnią liczbą całkowitą.", "expiration-time-min": "Minimalny czas to 60 sekund (1 minuta).", "refresh-expiration-time": "Czas wygaśnięcia tokena odświeżenia (s)", "refresh-expiration-time-required": "Czas ważności tokena odświeżania jest wymagany.", - "refresh-expiration-time-pattern": "Czas wygaśnięcia tokena odświeżenia powinien być dodatnią liczbą całkowitą.", "refresh-expiration-time-min": "Minimalny czas to 900 sekund (15 minut).", "refresh-expiration-time-less-token": "Czas odświeżania tokenu musi być dłuższy.", "generate-key": "Wygeneruj klucz", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index ac302a78bb..112911820e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -504,11 +504,9 @@ "signings-key-base64": "签名密钥必须是Base64格式。", "expiration-time": "令牌过期时间(秒)", "expiration-time-required": "令牌过期时间是必填。", - "expiration-time-pattern": "令牌过期时间必须是一个正整数。", "expiration-time-min": "最小时间为60秒(1分钟)。", "refresh-expiration-time": "刷新令牌过期时间(秒)", "refresh-expiration-time-required": "刷新令牌过期时间必填。", - "refresh-expiration-time-pattern": "刷新令牌的过期时间必须是一个正整数。", "refresh-expiration-time-min": "最小时间为900秒(15分钟)。", "refresh-expiration-time-less-token": "刷新令牌时间必须大于令牌过期时间。", "generate-key": "生成密钥", From fc8ededea2a7a7d4c61840df971b927f750a2583 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 25 Nov 2024 18:21:21 +0200 Subject: [PATCH 47/77] UI: Fixed typo --- .../modules/home/pages/admin/security-settings.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html index 1e2af17ba7..fc5010e7a1 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.html @@ -259,8 +259,8 @@
-
- +
+ admin.jwt.expiration-time Date: Tue, 26 Nov 2024 13:42:11 +0100 Subject: [PATCH 48/77] ugrade refactoring due to comments --- .../main/data/upgrade/3.8.1/schema_update.sql | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/application/src/main/data/upgrade/3.8.1/schema_update.sql b/application/src/main/data/upgrade/3.8.1/schema_update.sql index 27703ec9ff..96106fa390 100644 --- a/application/src/main/data/upgrade/3.8.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.8.1/schema_update.sql @@ -27,20 +27,15 @@ UPDATE tb_user SET additional_info = (additional_info::jsonb - 'lastLoginTs' - ' -- UPDATE RULE NODE DEBUG MODE TO DEBUG STRATEGY START -ALTER TABLE rule_node - ADD COLUMN IF NOT EXISTS debug_failures boolean DEFAULT false; -ALTER TABLE rule_node - ADD COLUMN IF NOT EXISTS debug_all_until bigint NOT NULL DEFAULT 0; +ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS debug_failures boolean DEFAULT false; +ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS debug_all_until bigint NOT NULL DEFAULT 0; DO $$ BEGIN - IF EXISTS (SELECT 1 - FROM information_schema.columns - WHERE table_name = 'rule_node' AND column_name = 'debug_mode') THEN - UPDATE rule_node - SET debug_all_until = CASE WHEN debug_mode = true THEN extract(epoch from now() + 3600) * 1000 ELSE 0 END; - ALTER TABLE rule_node - DROP COLUMN debug_mode; + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'rule_node' AND column_name = 'debug_mode') + THEN + UPDATE rule_node SET debug_all_until = (extract(epoch from now()) + 3600) * 1000 WHERE debug_mode = true; + ALTER TABLE rule_node DROP COLUMN debug_mode; END IF; END $$; From 9d0905601186549fcba2014a6832e919098a8dcc Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 27 Nov 2024 09:05:06 +0200 Subject: [PATCH 49/77] UI: Refactoring HP long vertical connector --- .../json/system/scada_symbols/long-vertical-connector-hp.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg index 1eafa846f8..86235ff1c1 100644 --- a/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg @@ -1,4 +1,4 @@ -{ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="400" fill="none" version="1.1" viewBox="0 0 200 400"><tb:metadata xmlns=""><![CDATA[{ "title": "HP Long vertical connector", "description": "Long vertical connector", "widgetSizeX": 1, @@ -170,5 +170,5 @@ } ] } - + \ No newline at end of file From 6b6bbebab8355a7ac8f11ea353b9737bbcde3332 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 27 Nov 2024 19:28:24 +0200 Subject: [PATCH 50/77] Implement JS Module resources support. --- .../main/data/upgrade/3.8.1/schema_update.sql | 8 +- .../controller/ControllerConstants.java | 1 + .../controller/TbResourceController.java | 8 + .../server/common/data/ResourceSubType.java | 4 +- ui-ngx/src/app/core/http/resource.service.ts | 7 +- ui-ngx/src/app/core/services/menu.models.ts | 13 ++ .../dashboard-widget-select.component.html | 2 +- .../action/widget-action.component.html | 1 + .../components/widget/widget.component.ts | 59 +++--- .../home/pages/admin/admin-routing.module.ts | 39 ++++ .../modules/home/pages/admin/admin.module.ts | 20 +- .../js-library-table-config.resolver.ts | 170 +++++++++++++++++ .../js-library-table-header.component.html | 30 +++ .../js-library-table-header.component.ts | 42 +++++ .../admin/resource/js-resource.component.html | 117 ++++++++++++ .../admin/resource/js-resource.component.ts | 176 ++++++++++++++++++ .../resource/resources-library.component.html | 6 +- .../resource/resources-library.component.ts | 10 +- .../resources-table-header.component.ts | 2 +- .../components/file-input.component.html | 10 +- .../components/file-input.component.scss | 41 +++- .../shared/components/file-input.component.ts | 10 + .../js-func-module-row.component.html | 39 ++++ .../js-func-module-row.component.scss | 23 +++ .../js-func-module-row.component.ts | 156 ++++++++++++++++ .../components/js-func-modules.component.html | 66 +++++++ .../components/js-func-modules.component.scss | 74 ++++++++ .../components/js-func-modules.component.ts | 135 ++++++++++++++ .../shared/components/js-func.component.html | 14 +- .../shared/components/js-func.component.ts | 138 +++++++++++--- .../resource-autocomplete.component.html | 8 +- .../resource-autocomplete.component.ts | 12 +- .../app/shared/models/js-function.models.ts | 126 +++++++++++++ .../src/app/shared/models/resource.models.ts | 13 +- ui-ngx/src/app/shared/models/widget.models.ts | 3 +- ui-ngx/src/app/shared/shared.module.ts | 6 + .../assets/locale/locale.constant-en_US.json | 38 +++- ui-ngx/src/form.scss | 9 + 38 files changed, 1546 insertions(+), 90 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts create mode 100644 ui-ngx/src/app/shared/components/js-func-module-row.component.html create mode 100644 ui-ngx/src/app/shared/components/js-func-module-row.component.scss create mode 100644 ui-ngx/src/app/shared/components/js-func-module-row.component.ts create mode 100644 ui-ngx/src/app/shared/components/js-func-modules.component.html create mode 100644 ui-ngx/src/app/shared/components/js-func-modules.component.scss create mode 100644 ui-ngx/src/app/shared/components/js-func-modules.component.ts create mode 100644 ui-ngx/src/app/shared/models/js-function.models.ts diff --git a/application/src/main/data/upgrade/3.8.1/schema_update.sql b/application/src/main/data/upgrade/3.8.1/schema_update.sql index e4faca82a5..b32e57fde5 100644 --- a/application/src/main/data/upgrade/3.8.1/schema_update.sql +++ b/application/src/main/data/upgrade/3.8.1/schema_update.sql @@ -176,4 +176,10 @@ $$ END IF; ALTER TABLE qr_code_settings DROP COLUMN IF EXISTS android_config, DROP COLUMN IF EXISTS ios_config; END; -$$; \ No newline at end of file +$$; + +-- UPDATE RESOURCE JS_MODULE SUB TYPE START + +UPDATE resource SET resource_sub_type = 'EXTENSION' WHERE resource_type = 'JS_MODULE' AND resource_sub_type IS NULL; + +-- UPDATE RESOURCE JS_MODULE SUB TYPE END diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index c3666501b5..c7a59cfd83 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -125,6 +125,7 @@ public class ControllerConstants { protected static final String RESOURCE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the resource title."; protected static final String RESOURCE_TYPE = "A string value representing the resource type."; + protected static final String RESOURCE_SUB_TYPE = "A string value representing the resource sub-type."; protected static final String LWM2M_OBJECT_DESCRIPTION = "LwM2M Object is a object that includes information about the LwM2M model which can be used in transport configuration for the LwM2M device profile. "; diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index 3671cec189..0d4b988771 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -38,6 +38,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; @@ -70,6 +71,7 @@ import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DE import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_INFO_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_SUB_TYPE; import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TYPE; import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; @@ -229,6 +231,8 @@ public class TbResourceController extends BaseController { @RequestParam int page, @Parameter(description = RESOURCE_TYPE, schema = @Schema(allowableValues = {"LWM2M_MODEL", "JKS", "PKCS_12", "JS_MODULE"})) @RequestParam(required = false) String resourceType, + @Parameter(description = RESOURCE_SUB_TYPE, schema = @Schema(allowableValues = {"EXTENSION", "MODULE"})) + @RequestParam(required = false) String resourceSubType, @Parameter(description = RESOURCE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, @Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "title", "resourceType", "tenantId"})) @@ -241,8 +245,12 @@ public class TbResourceController extends BaseController { Set resourceTypes = new HashSet<>(); if (StringUtils.isNotEmpty(resourceType)) { resourceTypes.add(ResourceType.valueOf(resourceType)); + if (StringUtils.isNotEmpty(resourceSubType)) { + filter.resourceSubTypes(Set.of(ResourceSubType.valueOf(resourceSubType))); + } } else { Collections.addAll(resourceTypes, ResourceType.values()); + resourceTypes.remove(ResourceType.JS_MODULE); resourceTypes.remove(ResourceType.IMAGE); resourceTypes.remove(ResourceType.DASHBOARD); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java index 10a033f75d..17b9621d2b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceSubType.java @@ -17,5 +17,7 @@ package org.thingsboard.server.common.data; public enum ResourceSubType { IMAGE, - SCADA_SYMBOL + SCADA_SYMBOL, + EXTENSION, + MODULE } diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index 78f025e59d..f8e0920119 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -20,7 +20,7 @@ import { PageLink } from '@shared/models/page/page-link'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { forkJoin, Observable, of } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; -import { Resource, ResourceInfo, ResourceType, TBResourceScope } from '@shared/models/resource.models'; +import { Resource, ResourceInfo, ResourceSubType, ResourceType, TBResourceScope } from '@shared/models/resource.models'; import { catchError, mergeMap } from 'rxjs/operators'; import { isNotEmptyStr } from '@core/utils'; import { ResourcesService } from '@core/services/resources.service'; @@ -36,11 +36,14 @@ export class ResourceService { } - public getResources(pageLink: PageLink, resourceType?: ResourceType, config?: RequestConfig): Observable> { + public getResources(pageLink: PageLink, resourceType?: ResourceType, resourceSubType?: ResourceSubType, config?: RequestConfig): Observable> { let url = `/api/resource${pageLink.toQuery()}`; if (isNotEmptyStr(resourceType)) { url += `&resourceType=${resourceType}`; } + if (isNotEmptyStr(resourceSubType)) { + url += `&resourceSubType=${resourceSubType}`; + } return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index cf08fe2a56..16a87770ac 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -59,6 +59,7 @@ export enum MenuId { images = 'images', scada_symbols = 'scada_symbols', resources_library = 'resources_library', + javascript_library = 'javascript_library', notifications_center = 'notifications_center', notification_inbox = 'notification_inbox', notification_sent = 'notification_sent', @@ -209,6 +210,16 @@ export const menuSectionMap = new Map([ icon: 'mdi:rhombus-split' } ], + [ + MenuId.javascript_library, + { + id: MenuId.javascript_library, + name: 'javascript.javascript-library', + type: 'link', + path: '/resources/javascript-library', + icon: 'mdi:language-javascript' + } + ], [ MenuId.notifications_center, { @@ -707,6 +718,7 @@ const defaultUserMenuMap = new Map([ }, {id: MenuId.images}, {id: MenuId.scada_symbols}, + {id: MenuId.javascript_library}, {id: MenuId.resources_library} ] }, @@ -803,6 +815,7 @@ const defaultUserMenuMap = new Map([ }, {id: MenuId.images}, {id: MenuId.scada_symbols}, + {id: MenuId.javascript_library}, {id: MenuId.resources_library} ] }, diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html index bfaaf700ce..f1bbca0292 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html @@ -51,7 +51,7 @@
- {{ item.title }} + {{ item.title }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html index 1b3213b251..362e8eac2e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html @@ -237,6 +237,7 @@ [globalVariables]="functionScopeVariables" [validationArgs]="[]" [editorCompleter]="customActionEditorCompleter" + withModules helpId="widget/action/custom_action_fn" > diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 7329eb1eaf..a9634c24a8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -120,6 +120,7 @@ import { DASHBOARD_PAGE_COMPONENT_TOKEN } from '@home/components/tokens'; import { MODULES_MAP } from '@shared/models/constants'; import { IModulesMap } from '@modules/common/modules-map.models'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; +import { compileTbFunction, isNotEmptyTbFunction } from '@shared/models/js-function.models'; @Component({ selector: 'tb-widget', @@ -1108,17 +1109,25 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, break; case WidgetActionType.custom: const customFunction = descriptor.customFunction; - if (customFunction && customFunction.length > 0) { - try { - if (!additionalParams) { - additionalParams = {}; + if (isNotEmptyTbFunction(customFunction)) { + compileTbFunction(this.widgetContext.http, customFunction, '$event', 'widgetContext', 'entityId', + 'entityName', 'additionalParams', 'entityLabel').subscribe( + { + next: (compiled) => { + try { + if (!additionalParams) { + additionalParams = {}; + } + compiled.execute($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); + } catch (e) { + console.error(e); + } + }, + error: (err) => { + console.error(err); + } } - const customActionFunction = new Function('$event', 'widgetContext', 'entityId', - 'entityName', 'additionalParams', 'entityLabel', customFunction); - customActionFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); - } catch (e) { - console.error(e); - } + ) } break; case WidgetActionType.customPretty: @@ -1133,18 +1142,26 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, } this.loadCustomActionResources(actionNamespace, customCss, customResources, descriptor).subscribe({ next: () => { - if (isDefined(customPrettyFunction) && customPrettyFunction.length > 0) { - try { - if (!additionalParams) { - additionalParams = {}; + if (isNotEmptyTbFunction(customPrettyFunction)) { + compileTbFunction(this.widgetContext.http, customPrettyFunction, '$event', 'widgetContext', 'entityId', + 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel').subscribe( + { + next: (compiled) => { + try { + if (!additionalParams) { + additionalParams = {}; + } + this.widgetContext.customDialog.setAdditionalImports(descriptor.customImports); + compiled.execute($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel); + } catch (e) { + console.error(e); + } + }, + error: (err) => { + console.error(err); + } } - const customActionPrettyFunction = new Function('$event', 'widgetContext', 'entityId', - 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel', customPrettyFunction); - this.widgetContext.customDialog.setAdditionalImports(descriptor.customImports); - customActionPrettyFunction($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel); - } catch (e) { - console.error(e); - } + ) } }, error: (errorMessages: string[]) => { diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index b914ab8b2a..7920e5b4ae 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -45,6 +45,7 @@ import { ImageService } from '@core/http/image.service'; import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol-editor.models'; import { MenuId } from '@core/services/menu.models'; import { catchError } from 'rxjs/operators'; +import { JsLibraryTableConfigResolver } from '@home/pages/admin/resource/js-library-table-config.resolver'; export const scadaSymbolResolver: ResolveFn = (route: ActivatedRouteSnapshot, @@ -177,6 +178,43 @@ const routes: Routes = [ } } ] + }, + { + path: 'javascript-library', + data: { + breadcrumb: { + menuId: MenuId.javascript_library + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'javascript.javascript-library', + }, + resolve: { + entitiesTableConfig: JsLibraryTableConfigResolver + } + }, + { + path: ':entityId', + component: EntityDetailsPageComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + breadcrumb: { + labelFunction: entityDetailsPageBreadcrumbLabelFunction, + icon: 'mdi:language-javascript' + } as BreadCrumbConfig, + auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], + title: 'javascript.javascript-library' + }, + resolve: { + entitiesTableConfig: JsLibraryTableConfigResolver + } + } + ] } ] }, @@ -393,6 +431,7 @@ const routes: Routes = [ exports: [RouterModule], providers: [ ResourcesLibraryTableConfigResolver, + JsLibraryTableConfigResolver, QueuesTableConfigResolver ] }) diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index 5613e953c3..704f909809 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -33,6 +33,9 @@ import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-a import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component'; import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component'; import { OAuth2Module } from '@home/pages/admin/oauth2/oauth2.module'; +import { JsLibraryTableHeaderComponent } from '@home/pages/admin/resource/js-library-table-header.component'; +import { JsResourceComponent } from '@home/pages/admin/resource/js-resource.component'; +import { NgxFlowModule } from '@flowjs/ngx-flow'; @NgModule({ declarations: @@ -45,17 +48,20 @@ import { OAuth2Module } from '@home/pages/admin/oauth2/oauth2.module'; HomeSettingsComponent, ResourcesLibraryComponent, ResourcesTableHeaderComponent, + JsResourceComponent, + JsLibraryTableHeaderComponent, QueueComponent, RepositoryAdminSettingsComponent, AutoCommitAdminSettingsComponent, TwoFactorAuthSettingsComponent ], - imports: [ - CommonModule, - SharedModule, - HomeComponentsModule, - AdminRoutingModule, - OAuth2Module - ] + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + AdminRoutingModule, + OAuth2Module, + NgxFlowModule + ] }) export class AdminModule { } diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts new file mode 100644 index 0000000000..5f48532eff --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts @@ -0,0 +1,170 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { Router } from '@angular/router'; +import { + Resource, + ResourceInfo, + ResourceSubType, + ResourceSubTypeTranslationMap, + ResourceType +} from '@shared/models/resource.models'; +import { EntityType, entityTypeResources } from '@shared/models/entity-type.models'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { DatePipe } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; +import { ResourceService } from '@core/http/resource.service'; +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Authority } from '@shared/models/authority.enum'; +import { PageLink } from '@shared/models/page/page-link'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { JsLibraryTableHeaderComponent } from '@home/pages/admin/resource/js-library-table-header.component'; +import { JsResourceComponent } from '@home/pages/admin/resource/js-resource.component'; +import { switchMap } from 'rxjs/operators'; + +@Injectable() +export class JsLibraryTableConfigResolver { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private store: Store, + private resourceService: ResourceService, + private translate: TranslateService, + private router: Router, + private datePipe: DatePipe) { + + this.config.entityType = EntityType.TB_RESOURCE; + this.config.entityComponent = JsResourceComponent; + this.config.entityTranslations = { + details: 'javascript.javascript-resource-details', + add: 'javascript.add', + noEntities: 'javascript.no-javascript-resource-text', + search: 'javascript.search', + selectedEntities: 'javascript.selected-javascript-resources' + }; + this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE); + this.config.headerComponent = JsLibraryTableHeaderComponent; + + this.config.entityTitle = (resource) => resource ? + resource.title : ''; + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('title', 'resource.title', '60%'), + new EntityTableColumn('resourceSubType', 'javascript.javascript-type', '40%', + entity => this.translate.instant(ResourceSubTypeTranslationMap.get(entity.resourceSubType))), + new EntityTableColumn('tenantId', 'resource.system', '60px', + entity => checkBoxCell(entity.tenantId.id === NULL_UUID)), + ); + + this.config.cellActionDescriptors.push( + { + name: this.translate.instant('javascript.download'), + icon: 'file_download', + isEnabled: () => true, + onAction: ($event, entity) => this.downloadResource($event, entity) + } + ); + + this.config.deleteEntityTitle = resource => this.translate.instant('javascript.delete-javascript-resource-title', + { resourceTitle: resource.title }); + this.config.deleteEntityContent = () => this.translate.instant('javascript.delete-javascript-resource-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('javascript.delete-javascript-resources-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('javascript.delete-javascript-resources-text'); + + this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink, ResourceType.JS_MODULE, this.config.componentsData.resourceSubType); + this.config.loadEntity = id => { + const current = this.config.getTable()?.dataSource?.currentEntity as ResourceInfo; + if (!current || current?.resourceSubType === ResourceSubType.MODULE) { + return this.resourceService.getResource(id.id); + } else { + return this.resourceService.getResourceInfoById(id.id) + } + }; + this.config.saveEntity = resource => { + let saveObservable = this.resourceService.saveResource(resource); + if (resource.resourceSubType === ResourceSubType.MODULE) { + saveObservable = saveObservable.pipe( + switchMap((saved) => this.resourceService.getResource(saved.id.id)) + ); + } + return saveObservable; + }; + this.config.deleteEntity = id => this.resourceService.deleteResource(id.id); + + this.config.onEntityAction = action => this.onResourceAction(action); + } + + resolve(): EntityTableConfig { + this.config.tableTitle = this.translate.instant('javascript.javascript-library'); + this.config.componentsData = { + resourceSubType: '' + }; + const authUser = getCurrentAuthUser(this.store); + this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); + this.config.entitySelectionEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); + this.config.detailsReadonly = (resource) => this.detailsReadonly(resource, authUser.authority); + return this.config; + } + + private openResource($event: Event, resourceInfo: ResourceInfo) { + if ($event) { + $event.stopPropagation(); + } + const url = this.router.createUrlTree(['resources', 'javascript-library', resourceInfo.id.id]); + this.router.navigateByUrl(url).then(() => {}); + } + + downloadResource($event: Event, resource: ResourceInfo) { + if ($event) { + $event.stopPropagation(); + } + this.resourceService.downloadResource(resource.id.id).subscribe(); + } + + onResourceAction(action: EntityAction): boolean { + switch (action.action) { + case 'open': + this.openResource(action.event, action.entity); + return true; + case 'downloadResource': + this.downloadResource(action.event, action.entity); + return true; + } + return false; + } + + private detailsReadonly(resource: ResourceInfo, authority: Authority): boolean { + return !this.isResourceEditable(resource, authority); + } + + private isResourceEditable(resource: ResourceInfo, authority: Authority): boolean { + if (authority === Authority.TENANT_ADMIN) { + return resource && resource.tenantId && resource.tenantId.id !== NULL_UUID; + } else { + return authority === Authority.SYS_ADMIN; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.html new file mode 100644 index 0000000000..c69ee84386 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.html @@ -0,0 +1,30 @@ + + + javascript.javascript-type + + + {{ "javascript.all-types" | translate }} + + + {{ resourceSubTypesTranslationMap.get(jsResourceSubType) | translate }} + + + diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.ts new file mode 100644 index 0000000000..d3718c3203 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-header.component.ts @@ -0,0 +1,42 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { Resource, ResourceInfo, ResourceSubType, ResourceSubTypeTranslationMap } from '@shared/models/resource.models'; +import { PageLink } from '@shared/models/page/page-link'; + +@Component({ + selector: 'tb-js-library-table-header', + templateUrl: './js-library-table-header.component.html', + styleUrls: [] +}) +export class JsLibraryTableHeaderComponent extends EntityTableHeaderComponent { + + readonly jsResourceSubTypes: ResourceSubType[] = [ResourceSubType.EXTENSION, ResourceSubType.MODULE]; + readonly resourceSubTypesTranslationMap = ResourceSubTypeTranslationMap; + + constructor(protected store: Store) { + super(store); + } + + jsResourceSubTypeChanged(resourceSubType: ResourceSubType) { + this.entitiesTableConfig.componentsData.resourceSubType = resourceSubType; + this.entitiesTableConfig.getTable().resetSortAndFilter(true); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html new file mode 100644 index 0000000000..d587629352 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.html @@ -0,0 +1,117 @@ + +
+ + + +
+ +
+
+
+ +
+ + javascript.javascript-type + + + {{ ResourceSubTypeTranslationMap.get(resourceSubType) | translate }} + + + + + resource.title + + + {{ 'resource.title-required' | translate }} + + + {{ 'resource.title-max-length' | translate }} + + + + + +
+ + + +
+
+
+ + resource.file-name + + +
+
+ +
diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts new file mode 100644 index 0000000000..30ab7eb5e0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-resource.component.ts @@ -0,0 +1,176 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { Subject } from 'rxjs'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { + Resource, + ResourceSubType, + ResourceSubTypeTranslationMap, + ResourceType, + ResourceTypeExtension, + ResourceTypeMIMETypes +} from '@shared/models/resource.models'; +import { startWith, takeUntil } from 'rxjs/operators'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { isDefinedAndNotNull } from '@core/utils'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { scadaSymbolGeneralStateHighlightRules } from '@home/pages/scada-symbol/scada-symbol-editor.models'; + +@Component({ + selector: 'tb-js-resource', + templateUrl: './js-resource.component.html' +}) +export class JsResourceComponent extends EntityComponent implements OnInit, OnDestroy { + + readonly ResourceSubType = ResourceSubType; + readonly jsResourceSubTypes: ResourceSubType[] = [ResourceSubType.EXTENSION, ResourceSubType.MODULE]; + readonly ResourceSubTypeTranslationMap = ResourceSubTypeTranslationMap; + readonly maxResourceSize = getCurrentAuthState(this.store).maxResourceSize; + + private destroy$ = new Subject(); + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: Resource, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + public fb: FormBuilder, + protected cd: ChangeDetectorRef) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + } + + ngOnInit(): void { + super.ngOnInit(); + if (this.isAdd) { + this.observeResourceSubTypeChange(); + } + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + this.destroy$.next(); + this.destroy$.complete(); + } + + hideDelete(): boolean { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildForm(entity: Resource): FormGroup { + return this.fb.group({ + title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], + resourceSubType: [entity?.resourceSubType ? entity.resourceSubType : ResourceSubType.EXTENSION, Validators.required], + fileName: [entity ? entity.fileName : null, Validators.required], + data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []], + content: [entity?.data?.length ? window.atob(entity.data) : '', Validators.required] + }); + } + + updateForm(entity: Resource): void { + this.entityForm.patchValue(entity); + const content = entity.resourceSubType === ResourceSubType.MODULE && entity?.data?.length ? window.atob(entity.data) : ''; + this.entityForm.get('content').patchValue(content); + } + + override updateFormState(): void { + super.updateFormState(); + if (this.isEdit && this.entityForm && !this.isAdd) { + this.entityForm.get('resourceSubType').disable({ emitEvent: false }); + this.updateResourceSubTypeFieldsState(this.entityForm.get('resourceSubType').value); + } + } + + prepareFormValue(formValue: Resource): Resource { + if (this.isEdit && !isDefinedAndNotNull(formValue.data)) { + delete formValue.data; + } + if (formValue.resourceSubType === ResourceSubType.MODULE) { + if (!formValue.fileName) { + formValue.fileName = formValue.title + '.js'; + } + formValue.data = window.btoa((formValue as any).content); + delete (formValue as any).content; + } + return super.prepareFormValue(formValue); + } + + getAllowedExtensions(): string { + return ResourceTypeExtension.get(ResourceType.JS_MODULE); + } + + getAcceptType(): string { + return ResourceTypeMIMETypes.get(ResourceType.JS_MODULE); + } + + convertToBase64File(data: string): string { + return window.btoa(data); + } + + onResourceIdCopied(): void { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('resource.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + + uploadContentFromFile(content: string) { + this.entityForm.get('content').patchValue(content); + this.entityForm.markAsDirty(); + } + + private observeResourceSubTypeChange(): void { + this.entityForm.get('resourceSubType').valueChanges.pipe( + startWith(ResourceSubType.EXTENSION), + takeUntil(this.destroy$) + ).subscribe((subType: ResourceSubType) => this.onResourceSubTypeChange(subType)); + } + + private onResourceSubTypeChange(subType: ResourceSubType): void { + this.updateResourceSubTypeFieldsState(subType); + this.entityForm.patchValue({ + data: null, + fileName: null + }, {emitEvent: false}); + } + + private updateResourceSubTypeFieldsState(subType: ResourceSubType) { + if (subType === ResourceSubType.EXTENSION) { + this.entityForm.get('data').enable({ emitEvent: false }); + this.entityForm.get('fileName').enable({ emitEvent: false }); + this.entityForm.get('content').disable({ emitEvent: false }); + } else { + this.entityForm.get('data').disable({ emitEvent: false }); + this.entityForm.get('fileName').disable({ emitEvent: false }); + this.entityForm.get('content').enable({ emitEvent: false }); + } + } + + protected readonly highlightRules = scadaSymbolGeneralStateHighlightRules; +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html index 0389d32aab..6f60c5b1c5 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html @@ -66,9 +66,9 @@ {{ 'resource.title-max-length' | translate }}
- -
+
resource.file-name diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts index e9b602e7d3..df5c311bc9 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts @@ -41,7 +41,7 @@ import { getCurrentAuthState } from '@core/auth/auth.selectors'; export class ResourcesLibraryComponent extends EntityComponent implements OnInit, OnDestroy { readonly resourceType = ResourceType; - readonly resourceTypes: ResourceType[] = Object.values(this.resourceType); + readonly resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS]; readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; readonly maxResourceSize = getCurrentAuthState(this.store).maxResourceSize; @@ -80,7 +80,7 @@ export class ResourcesLibraryComponent extends EntityComponent impleme buildForm(entity: Resource): FormGroup { return this.fb.group({ title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], - resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.JS_MODULE, Validators.required], + resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, Validators.required], fileName: [entity ? entity.fileName : null, Validators.required], data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []] }); @@ -94,9 +94,7 @@ export class ResourcesLibraryComponent extends EntityComponent impleme super.updateFormState(); if (this.isEdit && this.entityForm && !this.isAdd) { this.entityForm.get('resourceType').disable({ emitEvent: false }); - if (this.entityForm.get('resourceType').value !== ResourceType.JS_MODULE) { - this.entityForm.get('fileName').disable({ emitEvent: false }); - } + this.entityForm.get('fileName').disable({ emitEvent: false }); } } @@ -140,7 +138,7 @@ export class ResourcesLibraryComponent extends EntityComponent impleme private observeResourceTypeChange(): void { this.entityForm.get('resourceType').valueChanges.pipe( - startWith(ResourceType.JS_MODULE), + startWith(ResourceType.LWM2M_MODEL), takeUntil(this.destroy$) ).subscribe((type: ResourceType) => this.onResourceTypeChange(type)); } diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts index 85fab10661..c250d68fac 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts @@ -28,7 +28,7 @@ import { PageLink } from '@shared/models/page/page-link'; }) export class ResourcesTableHeaderComponent extends EntityTableHeaderComponent { - readonly resourceTypes: ResourceType[] = Object.values(ResourceType); + readonly resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS]; readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; constructor(protected store: Store) { diff --git a/ui-ngx/src/app/shared/components/file-input.component.html b/ui-ngx/src/app/shared/components/file-input.component.html index 6302caa36d..e4fa14f8a9 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.html +++ b/ui-ngx/src/app/shared/components/file-input.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-panel.component.ts index e9e7f084c3..b4c71fab5e 100644 --- a/ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-panel.component.ts @@ -15,6 +15,7 @@ /// import { + booleanAttribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -33,7 +34,6 @@ import { shareReplay, timer } from 'rxjs'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { DebugSettings } from '@shared/models/entity.models'; import { distinctUntilChanged, map, tap } from 'rxjs/operators'; -import { coerceBoolean } from '@shared/decorators/coercion'; @Component({ selector: 'tb-debug-settings-panel', @@ -49,8 +49,8 @@ import { coerceBoolean } from '@shared/decorators/coercion'; export class DebugSettingsPanelComponent extends PageComponent implements OnInit { @Input() popover: TbPopoverComponent; - @Input() @coerceBoolean() failuresEnabled = false; - @Input() @coerceBoolean() allEnabled = false; + @Input({ transform: booleanAttribute }) failuresEnabled = false; + @Input({ transform: booleanAttribute }) allEnabled = false; @Input() allEnabledUntil = 0; @Input() maxDebugModeDurationMinutes: number; @Input() debugLimitsConfiguration: string; From 0db51da6c07a4182fb352c947e887e7ab81db68b Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 29 Nov 2024 16:11:46 +0200 Subject: [PATCH 59/77] refactoring --- .../modules/home/pages/rulechain/rulechain-page.component.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 099f034202..35b10a27b3 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -1009,7 +1009,6 @@ export class RuleChainPageComponent extends PageComponent name: outputEdge.label, configuration: {}, additionalInfo: {}, - debugSettings: {}, singletonMode: false }; outputNode.additionalInfo.layoutX = Math.round(destNode.x); @@ -1055,7 +1054,6 @@ export class RuleChainPageComponent extends PageComponent configuration: { ruleChainId: ruleChain.id.id }, - debugSettings: {}, singletonMode: false, x: Math.round(ruleChainNodeX), y: Math.round(ruleChainNodeY), From 17af3809f05d873cd7a41a2c5b53fe7af3824763 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 29 Nov 2024 17:40:47 +0200 Subject: [PATCH 60/77] UI: Add support of js modules to: show widget header action function, data key post processing function. --- .../app/core/api/entity-data-subscription.ts | 671 +++++++++--------- .../src/app/core/api/entity-data.service.ts | 8 +- ui-ngx/src/app/core/services/utils.service.ts | 6 +- .../widget-action-dialog.component.html | 1 + .../data-key-config-dialog.component.ts | 11 +- .../config/data-key-config.component.html | 1 + .../config/data-key-config.component.ts | 17 +- .../alarm/alarms-table-widget.component.ts | 100 +-- .../entity/entities-table-widget.component.ts | 82 ++- .../widget/lib/table-widget.models.ts | 35 +- .../lib/timeseries-table-widget.component.ts | 178 +++-- .../components/widget/widget.component.ts | 74 +- .../home/models/dashboard-component.models.ts | 2 +- .../home/models/widget-component.models.ts | 3 +- .../shared/components/js-func.component.ts | 194 ++--- .../app/shared/models/js-function.models.ts | 29 +- ui-ngx/src/app/shared/models/widget.models.ts | 4 +- 17 files changed, 776 insertions(+), 640 deletions(-) diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index a1c88b3330..8df8b7c09b 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -19,14 +19,16 @@ import { DataEntry, DataSet, DataSetHolder, - DatasourceType, IndexedData, + DatasourceType, + IndexedData, widgetType } from '@shared/models/widget.models'; import { AggregationType, ComparisonDuration, createTimewindowForComparison, - getCurrentTime, IntervalMath, + getCurrentTime, + IntervalMath, SubscriptionTimewindow } from '@shared/models/time/time.models'; import { @@ -51,7 +53,8 @@ import { IndexedSubscriptionData, IntervalType, NOT_SUPPORTED, - SubscriptionData, SubscriptionDataEntry, + SubscriptionData, + SubscriptionDataEntry, TelemetrySubscriber } from '@shared/models/telemetry/telemetry.models'; import { UtilsService } from '@core/services/utils.service'; @@ -61,9 +64,16 @@ import { PageData } from '@shared/models/page/page-data'; import { DataAggregator, onAggregatedData } from '@core/api/data-aggregator'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { EntityType } from '@shared/models/entity-type.models'; -import { Observable, of, ReplaySubject, Subject } from 'rxjs'; +import { firstValueFrom, from, Observable, of, ReplaySubject, Subject, switchMap } from 'rxjs'; import { EntityId } from '@shared/models/id/entity-id'; import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; +import { + CompiledTbFunction, + compileTbFunction, + isNotEmptyTbFunction, + TbFunction +} from '@shared/models/js-function.models'; +import { HttpClient } from '@angular/common/http'; import Timeout = NodeJS.Timeout; declare type DataKeyFunction = (time: number, prevValue: any) => any; @@ -81,8 +91,8 @@ export interface SubscriptionDataKey { comparisonResultType?: ComparisonResultType; funcBody: string; func?: DataKeyFunction; - postFuncBody: string; - postFunc?: DataKeyPostFunction; + postFuncBody: TbFunction; + postFunc?: CompiledTbFunction; index?: number; listIndex?: number; key?: string; @@ -109,8 +119,8 @@ export class EntityDataSubscription { constructor(private listener: EntityDataListener, private telemetryService: TelemetryWebsocketService, - private utils: UtilsService) { - this.initializeSubscription(); + private utils: UtilsService, + private http: HttpClient) { } private entityDataSubscriptionOptions = this.listener.subscriptionOptions; @@ -193,7 +203,7 @@ export class EntityDataSubscription { return [[timestamp, value]]; } - private initializeSubscription() { + private async initializeSubscription() { for (let i = 0; i < this.entityDataSubscriptionOptions.dataKeys.length; i++) { const dataKey = deepClone(this.entityDataSubscriptionOptions.dataKeys[i]); this.dataKeysList.push(dataKey); @@ -203,9 +213,10 @@ export class EntityDataSubscription { dataKey.func = new Function('time', 'prevValue', dataKey.funcBody) as DataKeyFunction; } } else { - if (dataKey.postFuncBody && !dataKey.postFunc) { - dataKey.postFunc = new Function('time', 'value', 'prevValue', 'timePrev', 'prevOrigValue', - dataKey.postFuncBody) as DataKeyPostFunction; + if (isNotEmptyTbFunction(dataKey.postFuncBody) && !dataKey.postFunc) { + try { + dataKey.postFunc = await firstValueFrom(compileTbFunction(this.http, dataKey.postFuncBody, 'time', 'value', 'prevValue', 'timePrev', 'prevOrigValue')); + } catch (e) {} } } let key: string; @@ -265,354 +276,358 @@ export class EntityDataSubscription { } public subscribe(): Observable { - this.entityDataResolveSubject = new ReplaySubject(1); - if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { - this.started = true; - this.dataResolved = true; - this.prepareSubscriptionTimewindow(); - } - if (this.datasourceType === DatasourceType.entity) { - const entityFields: Array = - this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.entityField).map( - dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name }) - ); - if (!entityFields.find(key => key.key === 'name')) { - entityFields.push({ - type: EntityKeyType.ENTITY_FIELD, - key: 'name' - }); - } - if (!entityFields.find(key => key.key === 'label')) { - entityFields.push({ - type: EntityKeyType.ENTITY_FIELD, - key: 'label' - }); - } - if (!entityFields.find(key => key.key === 'additionalInfo')) { - entityFields.push({ - type: EntityKeyType.ENTITY_FIELD, - key: 'additionalInfo' - }); - } + return from(this.initializeSubscription()).pipe( + switchMap(() => { + this.entityDataResolveSubject = new ReplaySubject(1); + if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { + this.started = true; + this.dataResolved = true; + this.prepareSubscriptionTimewindow(); + } + if (this.datasourceType === DatasourceType.entity) { + const entityFields: Array = + this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.entityField).map( + dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name }) + ); + if (!entityFields.find(key => key.key === 'name')) { + entityFields.push({ + type: EntityKeyType.ENTITY_FIELD, + key: 'name' + }); + } + if (!entityFields.find(key => key.key === 'label')) { + entityFields.push({ + type: EntityKeyType.ENTITY_FIELD, + key: 'label' + }); + } + if (!entityFields.find(key => key.key === 'additionalInfo')) { + entityFields.push({ + type: EntityKeyType.ENTITY_FIELD, + key: 'additionalInfo' + }); + } - this.attrFields = this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.attribute).map( - dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name }) - ); + this.attrFields = this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.attribute).map( + dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name }) + ); - this.tsFields = this.dataKeysList. - filter(dataKey => dataKey.type === DataKeyType.timeseries && + this.tsFields = this.dataKeysList. + filter(dataKey => dataKey.type === DataKeyType.timeseries && (!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE) && !dataKey.latest).map( - dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) - ); + dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) + ); - if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { - const latestTsFields = this.dataKeysList. - filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.latest && - (!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE)).map( - dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) - ); - this.latestValues = this.attrFields.concat(latestTsFields); - } else { - this.latestValues = this.attrFields.concat(this.tsFields); - } + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + const latestTsFields = this.dataKeysList. + filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.latest && + (!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE)).map( + dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) + ); + this.latestValues = this.attrFields.concat(latestTsFields); + } else { + this.latestValues = this.attrFields.concat(this.tsFields); + } - this.aggTsValues = this.dataKeysList. + this.aggTsValues = this.dataKeysList. filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE && !dataKey.comparisonEnabled).map( dataKey => ({ id: dataKey.index, key: dataKey.name, agg: dataKey.aggregationType }) ); - this.aggTsComparisonValues = this.dataKeysList. - filter(dataKey => dataKey.type === DataKeyType.timeseries && - dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE && dataKey.comparisonEnabled).map( - dataKey => ({ id: dataKey.index, key: dataKey.name, agg: dataKey.aggregationType, - previousValueOnly: dataKey.comparisonResultType === ComparisonResultType.PREVIOUS_VALUE }) - ); + this.aggTsComparisonValues = this.dataKeysList. + filter(dataKey => dataKey.type === DataKeyType.timeseries && + dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE && dataKey.comparisonEnabled).map( + dataKey => ({ id: dataKey.index, key: dataKey.name, agg: dataKey.aggregationType, + previousValueOnly: dataKey.comparisonResultType === ComparisonResultType.PREVIOUS_VALUE }) + ); - this.subscriber = new TelemetrySubscriber(this.telemetryService); - this.dataCommand = new EntityDataCmd(); + this.subscriber = new TelemetrySubscriber(this.telemetryService); + this.dataCommand = new EntityDataCmd(); - let keyFilters = this.entityDataSubscriptionOptions.keyFilters; - if (this.entityDataSubscriptionOptions.additionalKeyFilters) { - if (keyFilters) { - keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); - } else { - keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; - } - } + let keyFilters = this.entityDataSubscriptionOptions.keyFilters; + if (this.entityDataSubscriptionOptions.additionalKeyFilters) { + if (keyFilters) { + keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); + } else { + keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; + } + } - this.dataCommand.query = { - entityFilter: this.entityDataSubscriptionOptions.entityFilter, - pageLink: this.entityDataSubscriptionOptions.pageLink, - keyFilters, - entityFields, - latestValues: this.latestValues - }; + this.dataCommand.query = { + entityFilter: this.entityDataSubscriptionOptions.entityFilter, + pageLink: this.entityDataSubscriptionOptions.pageLink, + keyFilters, + entityFields, + latestValues: this.latestValues + }; - if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { - this.prepareSubscriptionCommands(this.dataCommand); - if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { - this.subscriber.setTsOffset(this.subsTw.tsOffset); - } else { - this.subscriber.setTsOffset(this.latestTsOffset); - } - } + if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { + this.prepareSubscriptionCommands(this.dataCommand); + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + this.subscriber.setTsOffset(this.subsTw.tsOffset); + } else { + this.subscriber.setTsOffset(this.latestTsOffset); + } + } - this.subscriber.subscriptionCommands.push(this.dataCommand); + this.subscriber.subscriptionCommands.push(this.dataCommand); - this.subscriber.entityData$.subscribe( - (entityDataUpdate) => { - if (entityDataUpdate.data) { - this.onPageData(entityDataUpdate.data); - if (this.prematureUpdates) { - for (const update of this.prematureUpdates) { - this.onDataUpdate(update); + this.subscriber.entityData$.subscribe( + (entityDataUpdate) => { + if (entityDataUpdate.data) { + this.onPageData(entityDataUpdate.data); + if (this.prematureUpdates) { + for (const update of this.prematureUpdates) { + this.onDataUpdate(update); + } + this.prematureUpdates = null; + } + } else if (entityDataUpdate.update) { + if (!this.pageData) { + if (!this.prematureUpdates) { + this.prematureUpdates = []; + } + this.prematureUpdates.push(entityDataUpdate.update); + } else { + this.onDataUpdate(entityDataUpdate.update); + } } - this.prematureUpdates = null; } - } else if (entityDataUpdate.update) { - if (!this.pageData) { - if (!this.prematureUpdates) { - this.prematureUpdates = []; + ); + + this.subscriber.reconnect$.subscribe(() => { + if (this.started) { + const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand; + if (!this.history && (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length || + this.aggTsValues.length > 0 && !this.isFloatingTimewindow)) { + const newSubsTw = this.listener.updateRealtimeSubscription(); + this.subsTw = newSubsTw; + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length) { + targetCommand.tsCmd.startTs = this.subsTw.startTs; + targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow; + if (typeof this.subsTw.aggregation.interval === 'number') { + targetCommand.tsCmd.interval = this.subsTw.aggregation.interval; + targetCommand.tsCmd.intervalType = IntervalType.MILLISECONDS; + } else { + targetCommand.tsCmd.intervalType = this.subsTw.aggregation.interval; + } + targetCommand.tsCmd.timeZoneId = this.subsTw.timezone; + targetCommand.tsCmd.limit = this.subsTw.aggregation.limit; + targetCommand.tsCmd.agg = this.subsTw.aggregation.type; + targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData; + this.dataAggregators.forEach((dataAggregator) => { + dataAggregator.reset(newSubsTw); + }); + } + if (this.aggTsValues.length > 0 && !this.isFloatingTimewindow) { + targetCommand.aggTsCmd.startTs = this.subsTw.startTs; + targetCommand.aggTsCmd.timeWindow = this.subsTw.aggregation.timeWindow; + this.tsLatestDataAggregators.forEach((dataAggregator) => { + dataAggregator.reset(newSubsTw); + }); + } } - this.prematureUpdates.push(entityDataUpdate.update); + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + this.subscriber.setTsOffset(this.subsTw.tsOffset); + } else { + this.subscriber.setTsOffset(this.latestTsOffset); + } + targetCommand.query = this.dataCommand.query; + this.subscriber.subscriptionCommands = [targetCommand]; } else { - this.onDataUpdate(entityDataUpdate.update); + this.subscriber.subscriptionCommands = [this.dataCommand]; } + }); + this.subscriber.subscribe(); + } else if (this.datasourceType === DatasourceType.function) { + let tsOffset = 0; + if (this.entityDataSubscriptionOptions.type === widgetType.latest) { + tsOffset = this.entityDataSubscriptionOptions.latestTsOffset; + } else if (this.entityDataSubscriptionOptions.subscriptionTimewindow) { + tsOffset = this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset; } - } - ); - this.subscriber.reconnect$.subscribe(() => { - if (this.started) { - const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand; - if (!this.history && (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length || - this.aggTsValues.length > 0 && !this.isFloatingTimewindow)) { - const newSubsTw = this.listener.updateRealtimeSubscription(); - this.subsTw = newSubsTw; - if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length) { - targetCommand.tsCmd.startTs = this.subsTw.startTs; - targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow; - if (typeof this.subsTw.aggregation.interval === 'number') { - targetCommand.tsCmd.interval = this.subsTw.aggregation.interval; - targetCommand.tsCmd.intervalType = IntervalType.MILLISECONDS; + const entityData: EntityData = { + entityId: { + id: NULL_UUID, + entityType: EntityType.DEVICE + }, + timeseries: {}, + latest: {} + }; + const name = DatasourceType.function; + entityData.latest[EntityKeyType.ENTITY_FIELD] = { + name: {ts: Date.now() + tsOffset, value: name} + }; + const pageData: PageData = { + data: [entityData], + hasNext: false, + totalElements: 1, + totalPages: 1 + }; + this.onPageData(pageData); + } else if (this.datasourceType === DatasourceType.entityCount) { + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; + this.subscriber = new TelemetrySubscriber(this.telemetryService); + this.subscriber.setTsOffset(this.latestTsOffset); + this.countCommand = new EntityCountCmd(); + let keyFilters = this.entityDataSubscriptionOptions.keyFilters; + if (this.entityDataSubscriptionOptions.additionalKeyFilters) { + if (keyFilters) { + keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); + } else { + keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; + } + } + this.countCommand.query = { + entityFilter: this.entityDataSubscriptionOptions.entityFilter, + keyFilters + }; + this.subscriber.subscriptionCommands.push(this.countCommand); + + const entityId: EntityId = { + id: NULL_UUID, + entityType: null + }; + + const countKey = this.dataKeysList[0]; + + let dataReceived = false; + + this.subscriber.entityCount$.subscribe( + (entityCountUpdate) => { + if (!dataReceived) { + const entityData: EntityData = { + entityId, + latest: { + [EntityKeyType.ENTITY_FIELD]: { + name: { + ts: Date.now() + this.latestTsOffset, + value: DatasourceType.entityCount + } + }, + [EntityKeyType.COUNT]: { + [countKey.name]: { + ts: Date.now() + this.latestTsOffset, + value: entityCountUpdate.count + '' + } + } + }, + timeseries: {} + }; + const pageData: PageData = { + data: [entityData], + hasNext: false, + totalElements: 1, + totalPages: 1 + }; + this.onPageData(pageData); + dataReceived = true; } else { - targetCommand.tsCmd.intervalType = this.subsTw.aggregation.interval; + const update: EntityData[] = [{ + entityId, + latest: { + [EntityKeyType.COUNT]: { + [countKey.name]: { + ts: Date.now() + this.latestTsOffset, + value: entityCountUpdate.count + '' + } + } + }, + timeseries: {} + }]; + this.onDataUpdate(update); } - targetCommand.tsCmd.timeZoneId = this.subsTw.timezone; - targetCommand.tsCmd.limit = this.subsTw.aggregation.limit; - targetCommand.tsCmd.agg = this.subsTw.aggregation.type; - targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData; - this.dataAggregators.forEach((dataAggregator) => { - dataAggregator.reset(newSubsTw); - }); } - if (this.aggTsValues.length > 0 && !this.isFloatingTimewindow) { - targetCommand.aggTsCmd.startTs = this.subsTw.startTs; - targetCommand.aggTsCmd.timeWindow = this.subsTw.aggregation.timeWindow; - this.tsLatestDataAggregators.forEach((dataAggregator) => { - dataAggregator.reset(newSubsTw); - }); + ); + this.subscriber.subscribe(); + } else if (this.datasourceType === DatasourceType.alarmCount) { + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; + this.subscriber = new TelemetrySubscriber(this.telemetryService); + this.subscriber.setTsOffset(this.latestTsOffset); + this.alarmCountCommand = new AlarmCountCmd(); + let keyFilters = this.entityDataSubscriptionOptions.keyFilters; + if (this.entityDataSubscriptionOptions.additionalKeyFilters) { + if (keyFilters) { + keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); + } else { + keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; } } - if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { - this.subscriber.setTsOffset(this.subsTw.tsOffset); - } else { - this.subscriber.setTsOffset(this.latestTsOffset); + this.alarmCountCommand.query = { + entityFilter: this.entityDataSubscriptionOptions.entityFilter, + keyFilters + }; + if (this.entityDataSubscriptionOptions.alarmFilter) { + this.alarmCountCommand.query = {...this.alarmCountCommand.query, ...this.entityDataSubscriptionOptions.alarmFilter}; } - targetCommand.query = this.dataCommand.query; - this.subscriber.subscriptionCommands = [targetCommand]; - } else { - this.subscriber.subscriptionCommands = [this.dataCommand]; - } - }); - this.subscriber.subscribe(); - } else if (this.datasourceType === DatasourceType.function) { - let tsOffset = 0; - if (this.entityDataSubscriptionOptions.type === widgetType.latest) { - tsOffset = this.entityDataSubscriptionOptions.latestTsOffset; - } else if (this.entityDataSubscriptionOptions.subscriptionTimewindow) { - tsOffset = this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset; - } - - const entityData: EntityData = { - entityId: { - id: NULL_UUID, - entityType: EntityType.DEVICE - }, - timeseries: {}, - latest: {} - }; - const name = DatasourceType.function; - entityData.latest[EntityKeyType.ENTITY_FIELD] = { - name: {ts: Date.now() + tsOffset, value: name} - }; - const pageData: PageData = { - data: [entityData], - hasNext: false, - totalElements: 1, - totalPages: 1 - }; - this.onPageData(pageData); - } else if (this.datasourceType === DatasourceType.entityCount) { - this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; - this.subscriber = new TelemetrySubscriber(this.telemetryService); - this.subscriber.setTsOffset(this.latestTsOffset); - this.countCommand = new EntityCountCmd(); - let keyFilters = this.entityDataSubscriptionOptions.keyFilters; - if (this.entityDataSubscriptionOptions.additionalKeyFilters) { - if (keyFilters) { - keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); - } else { - keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; - } - } - this.countCommand.query = { - entityFilter: this.entityDataSubscriptionOptions.entityFilter, - keyFilters - }; - this.subscriber.subscriptionCommands.push(this.countCommand); + this.subscriber.subscriptionCommands.push(this.alarmCountCommand); - const entityId: EntityId = { - id: NULL_UUID, - entityType: null - }; - - const countKey = this.dataKeysList[0]; - - let dataReceived = false; + const entityId: EntityId = { + id: NULL_UUID, + entityType: null + }; - this.subscriber.entityCount$.subscribe( - (entityCountUpdate) => { - if (!dataReceived) { - const entityData: EntityData = { - entityId, - latest: { - [EntityKeyType.ENTITY_FIELD]: { - name: { - ts: Date.now() + this.latestTsOffset, - value: DatasourceType.entityCount - } - }, - [EntityKeyType.COUNT]: { - [countKey.name]: { - ts: Date.now() + this.latestTsOffset, - value: entityCountUpdate.count + '' - } - } - }, - timeseries: {} - }; - const pageData: PageData = { - data: [entityData], - hasNext: false, - totalElements: 1, - totalPages: 1 - }; - this.onPageData(pageData); - dataReceived = true; - } else { - const update: EntityData[] = [{ - entityId, - latest: { - [EntityKeyType.COUNT]: { - [countKey.name]: { - ts: Date.now() + this.latestTsOffset, - value: entityCountUpdate.count + '' - } - } - }, - timeseries: {} - }]; - this.onDataUpdate(update); - } + const countKey = this.dataKeysList[0]; + + let dataReceived = false; + + this.subscriber.alarmCount$.subscribe( + (alarmCountUpdate) => { + if (!dataReceived) { + const entityData: EntityData = { + entityId, + latest: { + [EntityKeyType.ENTITY_FIELD]: { + name: { + ts: Date.now() + this.latestTsOffset, + value: DatasourceType.alarmCount + } + }, + [EntityKeyType.COUNT]: { + [countKey.name]: { + ts: Date.now() + this.latestTsOffset, + value: alarmCountUpdate.count + '' + } + } + }, + timeseries: {} + }; + const pageData: PageData = { + data: [entityData], + hasNext: false, + totalElements: 1, + totalPages: 1 + }; + this.onPageData(pageData); + dataReceived = true; + } else { + const update: EntityData[] = [{ + entityId, + latest: { + [EntityKeyType.COUNT]: { + [countKey.name]: { + ts: Date.now() + this.latestTsOffset, + value: alarmCountUpdate.count + '' + } + } + }, + timeseries: {} + }]; + this.onDataUpdate(update); + } + } + ); + this.subscriber.subscribe(); } - ); - this.subscriber.subscribe(); - } else if (this.datasourceType === DatasourceType.alarmCount) { - this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; - this.subscriber = new TelemetrySubscriber(this.telemetryService); - this.subscriber.setTsOffset(this.latestTsOffset); - this.alarmCountCommand = new AlarmCountCmd(); - let keyFilters = this.entityDataSubscriptionOptions.keyFilters; - if (this.entityDataSubscriptionOptions.additionalKeyFilters) { - if (keyFilters) { - keyFilters = keyFilters.concat(this.entityDataSubscriptionOptions.additionalKeyFilters); + if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { + return of(null); } else { - keyFilters = this.entityDataSubscriptionOptions.additionalKeyFilters; + return this.entityDataResolveSubject.asObservable(); } - } - this.alarmCountCommand.query = { - entityFilter: this.entityDataSubscriptionOptions.entityFilter, - keyFilters - }; - if (this.entityDataSubscriptionOptions.alarmFilter) { - this.alarmCountCommand.query = {...this.alarmCountCommand.query, ...this.entityDataSubscriptionOptions.alarmFilter}; - } - this.subscriber.subscriptionCommands.push(this.alarmCountCommand); - - const entityId: EntityId = { - id: NULL_UUID, - entityType: null - }; - - const countKey = this.dataKeysList[0]; - - let dataReceived = false; - - this.subscriber.alarmCount$.subscribe( - (alarmCountUpdate) => { - if (!dataReceived) { - const entityData: EntityData = { - entityId, - latest: { - [EntityKeyType.ENTITY_FIELD]: { - name: { - ts: Date.now() + this.latestTsOffset, - value: DatasourceType.alarmCount - } - }, - [EntityKeyType.COUNT]: { - [countKey.name]: { - ts: Date.now() + this.latestTsOffset, - value: alarmCountUpdate.count + '' - } - } - }, - timeseries: {} - }; - const pageData: PageData = { - data: [entityData], - hasNext: false, - totalElements: 1, - totalPages: 1 - }; - this.onPageData(pageData); - dataReceived = true; - } else { - const update: EntityData[] = [{ - entityId, - latest: { - [EntityKeyType.COUNT]: { - [countKey.name]: { - ts: Date.now() + this.latestTsOffset, - value: alarmCountUpdate.count + '' - } - } - }, - timeseries: {} - }]; - this.onDataUpdate(update); - } - } - ); - this.subscriber.subscribe(); - } - if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { - return of(null); - } else { - return this.entityDataResolveSubject.asObservable(); - } + }) + ); } public start() { @@ -1104,7 +1119,7 @@ export class EntityDataSubscription { this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1], series[2]]); let value = EntityDataSubscription.convertValue(series[1]); if (dataKey.postFunc) { - value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); + value = dataKey.postFunc.execute(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); } prevOrigSeries = [series[0], series[1], series[2]]; series = [series[0], value, series[2]]; @@ -1119,7 +1134,7 @@ export class EntityDataSubscription { this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1], series[2]]); let value = EntityDataSubscription.convertValue(series[1]); if (dataKey.postFunc) { - value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); + value = dataKey.postFunc.execute(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); } series = [time, value, series[2]]; data.push([series[0], series[1], series[2]]); diff --git a/ui-ngx/src/app/core/api/entity-data.service.ts b/ui-ngx/src/app/core/api/entity-data.service.ts index 5ebad5e686..d631b0bbe0 100644 --- a/ui-ngx/src/app/core/api/entity-data.service.ts +++ b/ui-ngx/src/app/core/api/entity-data.service.ts @@ -28,6 +28,7 @@ import { SubscriptionDataKey } from '@core/api/entity-data-subscription'; import { Observable, of } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; export interface EntityDataListener { subscriptionType: widgetType; @@ -62,7 +63,8 @@ export interface EntityDataLoadResult { export class EntityDataService { constructor(private telemetryService: TelemetryWebsocketService, - private utils: UtilsService) {} + private utils: UtilsService, + private http: HttpClient) {} private static isUnresolvedDatasource(datasource: Datasource, pageLink: EntityDataPageLink): boolean { if (datasource.type === DatasourceType.entity) { @@ -103,7 +105,7 @@ export class EntityDataService { if (EntityDataService.isUnresolvedDatasource(datasource, datasource.pageLink)) { return of(null); } - listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils); + listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils, this.http); return listener.subscription.subscribe(); } @@ -137,7 +139,7 @@ export class EntityDataService { listener.configDatasourceIndex, listener.subscriptionOptions.pageLink); return of(null); } - listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils); + listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils, this.http); if (listener.useTimewindow) { listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); } diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index d9e8788cba..79fde27014 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -14,9 +14,6 @@ /// limitations under the License. /// -// eslint-disable-next-line @typescript-eslint/triple-slash-reference -/// - import { Inject, Injectable, NgZone, Renderer2 } from '@angular/core'; import { WINDOW } from '@core/services/window.service'; import { ExceptionData, parseException } from '@app/shared/models/error.models'; @@ -53,6 +50,7 @@ import { EntityId } from '@shared/models/id/entity-id'; import { DatePipe, DOCUMENT } from '@angular/common'; import { entityTypeTranslations } from '@shared/models/entity-type.models'; import cssjs from '@core/css/css'; +import { isNotEmptyTbFunction } from '@shared/models/js-function.models'; const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g'); @@ -291,7 +289,7 @@ export class UtilsService { } else if (index > -1) { dataKey.color = this.getMaterialColor(index); } - if (keyInfo.postFuncBody && keyInfo.postFuncBody.length) { + if (isNotEmptyTbFunction(keyInfo.postFuncBody)) { dataKey.usePostProcessing = true; dataKey.postFuncBody = keyInfo.postFuncBody; } diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html index d06493594e..621aebeae9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html @@ -107,6 +107,7 @@ { + if (this.dataKeyFormGroup.valid) { + const dataKey: DataKey = this.dataKeyFormGroup.get('dataKey').value; + this.dialogRef.close(dataKey); + } + }); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html index 1930016656..ade0765402 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html @@ -174,6 +174,7 @@ [functionArgs]="['time', 'value', 'prevValue', 'timePrev', 'prevOrigValue']" [globalVariables]="functionScopeVariables" [validationArgs]="[[1, 1, 1, 1, 1],[1, '1', '1', 1, '1']]" + withModules resultType="any" helpId="widget/config/datakey_postprocess_fn" formControlName="postFuncBody"> diff --git a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts index 4db2aaa8bf..32e8727225 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts @@ -57,6 +57,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetComponentService } from '@home/components/widget/widget-component.service'; import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; +import { isNotEmptyTbFunction, TbFunction } from '@shared/models/js-function.models'; @Component({ selector: 'tb-data-key-config', @@ -290,10 +291,10 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con }); this.dataKeyFormGroup.get('usePostProcessing').valueChanges.subscribe((usePostProcessing: boolean) => { - const postFuncBody: string = this.dataKeyFormGroup.get('postFuncBody').value; - if (usePostProcessing && (!postFuncBody || !postFuncBody.length)) { + const postFuncBody: TbFunction = this.dataKeyFormGroup.get('postFuncBody').value; + if (usePostProcessing && !isNotEmptyTbFunction(postFuncBody)) { this.dataKeyFormGroup.get('postFuncBody').patchValue('return value;'); - } else if (!usePostProcessing && postFuncBody && postFuncBody.length) { + } else if (!usePostProcessing && isNotEmptyTbFunction(postFuncBody)) { this.dataKeyFormGroup.get('postFuncBody').patchValue(null); } }); @@ -317,7 +318,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con writeValue(value: DataKey): void { this.modelValue = value; - if (this.modelValue.postFuncBody && this.modelValue.postFuncBody.length) { + if (isNotEmptyTbFunction(this.modelValue.postFuncBody)) { this.modelValue.usePostProcessing = true; } if (this.widgetType === widgetType.latest && this.modelValue.type === DataKeyType.timeseries && !this.modelValue.aggregationType) { @@ -466,13 +467,15 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con return key => key.toLowerCase().startsWith(lowercaseQuery); } - public validateOnSubmit() { + public validateOnSubmit(): Observable { if (this.modelValue.type === DataKeyType.function && this.funcBodyEdit) { - this.funcBodyEdit.validateOnSubmit(); + return this.funcBodyEdit.validateOnSubmit(); } else if ((this.modelValue.type === DataKeyType.timeseries || this.modelValue.type === DataKeyType.attribute) && this.dataKeyFormGroup.get('usePostProcessing').value && this.postFuncBodyEdit) { - this.postFuncBodyEdit.validateOnSubmit(); + return this.postFuncBodyEdit.validateOnSubmit(); + } else { + return of(null); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts index b5a081836d..86256a53bb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts @@ -51,7 +51,7 @@ import cssjs from '@core/css/css'; import { sortItems } from '@shared/models/page/page-link'; import { Direction } from '@shared/models/page/sort-order'; import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections'; -import { BehaviorSubject, forkJoin, fromEvent, merge, Observable, Subject, Subscription } from 'rxjs'; +import { BehaviorSubject, forkJoin, fromEvent, merge, Observable, of, Subject, Subscription } from 'rxjs'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { debounceTime, distinctUntilChanged, map, take, takeUntil, tap } from 'rxjs/operators'; import { MatPaginator } from '@angular/material/paginator'; @@ -1196,20 +1196,32 @@ class AlarmsDatasource implements DataSource { private reserveSpaceForHiddenAction = true; private cellButtonActions: TableCellButtonActionDescriptor[]; - private readonly usedShowCellActionFunction: boolean; + private usedShowCellActionFunction: boolean; + private inited = false; constructor(private subscription: IWidgetSubscription, private dataKeys: Array, private ngZone: NgZone, private widgetContext: WidgetContext, - actionCellDescriptors: AlarmWidgetActionDescriptor[]) { - this.cellButtonActions = actionCellDescriptors.concat(getTableCellButtonActions(widgetContext)); - this.usedShowCellActionFunction = this.cellButtonActions.some(action => action.useShowActionCellButtonFunction); + private actionCellDescriptors: AlarmWidgetActionDescriptor[]) { if (this.widgetContext.settings.reserveSpaceForHiddenAction) { this.reserveSpaceForHiddenAction = coerceBooleanProperty(this.widgetContext.settings.reserveSpaceForHiddenAction); } } + private init(): Observable { + if (this.inited) { + return of(null); + } + return getTableCellButtonActions(this.widgetContext).pipe( + tap(actions => { + this.cellButtonActions = this.actionCellDescriptors.concat(actions); + this.usedShowCellActionFunction = this.cellButtonActions.some(action => action.useShowActionCellButtonFunction); + this.inited = true; + }) + ); + } + connect(collectionViewer: CollectionViewer): Observable> { return this.alarmsSubject.asObservable(); } @@ -1237,47 +1249,49 @@ class AlarmsDatasource implements DataSource { } updateAlarms() { - const subscriptionAlarms = this.subscription.alarms; - let alarms = new Array(); - let maxCellButtonAction = 0; - let isEmptySelection = false; - const dynamicWidthCellButtonActions = this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction; - subscriptionAlarms.data.forEach((alarmData) => { - const alarm = this.alarmDataToInfo(alarmData); - alarms.push(alarm); - if (dynamicWidthCellButtonActions && alarm.actionCellButtons.length > maxCellButtonAction) { - maxCellButtonAction = alarm.actionCellButtons.length; + this.init().subscribe(() => { + const subscriptionAlarms = this.subscription.alarms; + let alarms = new Array(); + let maxCellButtonAction = 0; + let isEmptySelection = false; + const dynamicWidthCellButtonActions = this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction; + subscriptionAlarms.data.forEach((alarmData) => { + const alarm = this.alarmDataToInfo(alarmData); + alarms.push(alarm); + if (dynamicWidthCellButtonActions && alarm.actionCellButtons.length > maxCellButtonAction) { + maxCellButtonAction = alarm.actionCellButtons.length; + } + }); + if (!dynamicWidthCellButtonActions && this.cellButtonActions.length && alarms.length) { + maxCellButtonAction = alarms[0].actionCellButtons.length; } - }); - if (!dynamicWidthCellButtonActions && this.cellButtonActions.length && alarms.length) { - maxCellButtonAction = alarms[0].actionCellButtons.length; - } - if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) { - const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC; - alarms = alarms.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc)); - } - if (this.selection.hasValue()) { - const alarmIds = alarms.map((alarm) => alarm.id.id); - const toRemove = this.selection.selected.filter(alarm => alarmIds.indexOf(alarm.id.id) === -1); - this.selection.deselect(...toRemove); - if (this.selection.isEmpty()) { - isEmptySelection = true; + if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) { + const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC; + alarms = alarms.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc)); } - } - const alarmsPageData: PageData = { - data: alarms, - totalPages: subscriptionAlarms.totalPages, - totalElements: subscriptionAlarms.totalElements, - hasNext: subscriptionAlarms.hasNext - }; - this.ngZone.run(() => { - if (isEmptySelection) { - this.onSelectionModeChanged(false); + if (this.selection.hasValue()) { + const alarmIds = alarms.map((alarm) => alarm.id.id); + const toRemove = this.selection.selected.filter(alarm => alarmIds.indexOf(alarm.id.id) === -1); + this.selection.deselect(...toRemove); + if (this.selection.isEmpty()) { + isEmptySelection = true; + } } - this.alarmsSubject.next(alarms); - this.pageDataSubject.next(alarmsPageData); - this.countCellButtonAction = maxCellButtonAction; - this.dataLoading = false; + const alarmsPageData: PageData = { + data: alarms, + totalPages: subscriptionAlarms.totalPages, + totalElements: subscriptionAlarms.totalElements, + hasNext: subscriptionAlarms.hasNext + }; + this.ngZone.run(() => { + if (isEmptySelection) { + this.onSelectionModeChanged(false); + } + this.alarmsSubject.next(alarms); + this.pageDataSubject.next(alarmsPageData); + this.countCellButtonAction = maxCellButtonAction; + this.dataLoading = false; + }); }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts index 8c44ea759a..2f4c55beab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.ts @@ -46,11 +46,11 @@ import { deepClone, hashCode, isDefined, isNumber, isObject, isUndefined } from import cssjs from '@core/css/css'; import { CollectionViewer, DataSource } from '@angular/cdk/collections'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; -import { BehaviorSubject, fromEvent, merge, Observable, Subject } from 'rxjs'; +import { BehaviorSubject, fromEvent, merge, Observable, of, Subject } from 'rxjs'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; -import { debounceTime, distinctUntilChanged, map, takeUntil } from 'rxjs/operators'; +import { debounceTime, distinctUntilChanged, map, takeUntil, tap } from 'rxjs/operators'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort, SortDirection } from '@angular/material/sort'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @@ -789,7 +789,8 @@ class EntityDatasource implements DataSource { private reserveSpaceForHiddenAction = true; private cellButtonActions: TableCellButtonActionDescriptor[]; - private readonly usedShowCellActionFunction: boolean; + private usedShowCellActionFunction: boolean; + private inited = false; constructor( private translate: TranslateService, @@ -798,13 +799,24 @@ class EntityDatasource implements DataSource { private ngZone: NgZone, private widgetContext: WidgetContext ) { - this.cellButtonActions = getTableCellButtonActions(widgetContext); - this.usedShowCellActionFunction = this.cellButtonActions.some(action => action.useShowActionCellButtonFunction); if (this.widgetContext.settings.reserveSpaceForHiddenAction) { this.reserveSpaceForHiddenAction = coerceBooleanProperty(this.widgetContext.settings.reserveSpaceForHiddenAction); } } + private init(): Observable { + if (this.inited) { + return of(null); + } + return getTableCellButtonActions(this.widgetContext).pipe( + tap(actions => { + this.cellButtonActions = actions + this.usedShowCellActionFunction = this.cellButtonActions.some(action => action.useShowActionCellButtonFunction); + this.inited = true; + }) + ); + } + connect(collectionViewer: CollectionViewer): Observable> { return this.entitiesSubject.asObservable(); } @@ -828,36 +840,38 @@ class EntityDatasource implements DataSource { } dataUpdated() { - const datasourcesPageData = this.subscription.datasourcePages[0]; - const dataPageData = this.subscription.dataPages[0]; - let entities = new Array(); - let maxCellButtonAction = 0; - const dynamicWidthCellButtonActions = this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction; - datasourcesPageData.data.forEach((datasource, index) => { - const entity = this.datasourceToEntityData(datasource, dataPageData.data[index]); - entities.push(entity); - if (dynamicWidthCellButtonActions && entity.actionCellButtons.length > maxCellButtonAction) { - maxCellButtonAction = entity.actionCellButtons.length; + this.init().subscribe(() => { + const datasourcesPageData = this.subscription.datasourcePages[0]; + const dataPageData = this.subscription.dataPages[0]; + let entities = new Array(); + let maxCellButtonAction = 0; + const dynamicWidthCellButtonActions = this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction; + datasourcesPageData.data.forEach((datasource, index) => { + const entity = this.datasourceToEntityData(datasource, dataPageData.data[index]); + entities.push(entity); + if (dynamicWidthCellButtonActions && entity.actionCellButtons.length > maxCellButtonAction) { + maxCellButtonAction = entity.actionCellButtons.length; + } + }); + if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) { + const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC; + entities = entities.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc)); } - }); - if (this.appliedSortOrderLabel && this.appliedSortOrderLabel.length) { - const asc = this.appliedPageLink.sortOrder.direction === Direction.ASC; - entities = entities.sort((a, b) => sortItems(a, b, this.appliedSortOrderLabel, asc)); - } - if (!dynamicWidthCellButtonActions && this.cellButtonActions.length && entities.length) { - maxCellButtonAction = entities[0].actionCellButtons.length; - } - const entitiesPageData: PageData = { - data: entities, - totalPages: datasourcesPageData.totalPages, - totalElements: datasourcesPageData.totalElements, - hasNext: datasourcesPageData.hasNext - }; - this.ngZone.run(() => { - this.entitiesSubject.next(entities); - this.pageDataSubject.next(entitiesPageData); - this.countCellButtonAction = maxCellButtonAction; - this.dataLoading = false; + if (!dynamicWidthCellButtonActions && this.cellButtonActions.length && entities.length) { + maxCellButtonAction = entities[0].actionCellButtons.length; + } + const entitiesPageData: PageData = { + data: entities, + totalPages: datasourcesPageData.totalPages, + totalElements: datasourcesPageData.totalElements, + hasNext: datasourcesPageData.hasNext + }; + this.ngZone.run(() => { + this.entitiesSubject.next(entities); + this.pageDataSubject.next(entitiesPageData); + this.countCellButtonAction = maxCellButtonAction; + this.dataLoading = false; + }); }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index 3e200481fe..b31b9d9e5a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -26,6 +26,9 @@ import { WidgetContext } from '@home/models/widget-component.models'; import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; +import { CompiledTbFunction, compileTbFunction, isNotEmptyTbFunction } from '@shared/models/js-function.models'; +import { forkJoin, Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; type ColumnVisibilityOptions = 'visible' | 'hidden' | 'hidden-mobile'; @@ -59,7 +62,7 @@ export type ShowCellButtonActionFunction = (ctx: WidgetContext, data: EntityData export interface TableCellButtonActionDescriptor extends WidgetActionDescriptor { useShowActionCellButtonFunction: boolean; - showActionCellButtonFunction: ShowCellButtonActionFunction; + showActionCellButtonFunction: CompiledTbFunction; } export interface EntityData { @@ -314,20 +317,26 @@ export function getColumnSelectionAvailability(keySettings: TableWidgetDataKeySe return !(isDefined(keySettings.columnSelectionToDisplay) && keySettings.columnSelectionToDisplay === 'disabled'); } -export function getTableCellButtonActions(widgetContext: WidgetContext): TableCellButtonActionDescriptor[] { - return widgetContext.actionsApi.getActionDescriptors('actionCellButton').map(descriptor => { +export function getTableCellButtonActions(widgetContext: WidgetContext): Observable { + const actions$ = widgetContext.actionsApi.getActionDescriptors('actionCellButton').map(descriptor => { let useShowActionCellButtonFunction = descriptor.useShowWidgetActionFunction || false; - let showActionCellButtonFunction: ShowCellButtonActionFunction = null; - if (useShowActionCellButtonFunction && isNotEmptyStr(descriptor.showWidgetActionFunction)) { - try { - showActionCellButtonFunction = - new Function('widgetContext', 'data', descriptor.showWidgetActionFunction) as ShowCellButtonActionFunction; - } catch (e) { - useShowActionCellButtonFunction = false; - } + let showActionCellButtonFunction$: Observable>; + if (useShowActionCellButtonFunction && isNotEmptyTbFunction(descriptor.showWidgetActionFunction)) { + showActionCellButtonFunction$ = compileTbFunction(widgetContext.http, descriptor.showWidgetActionFunction, 'widgetContext', 'data'); + } else { + showActionCellButtonFunction$ = of(null); } - return {...descriptor, showActionCellButtonFunction, useShowActionCellButtonFunction}; + return showActionCellButtonFunction$.pipe( + catchError(() => { return of(null) }), + map(showActionCellButtonFunction => { + if (!showActionCellButtonFunction) { + useShowActionCellButtonFunction = false; + } + return {...descriptor, showActionCellButtonFunction, useShowActionCellButtonFunction}; + }) + ); }); + return actions$.length ? forkJoin(actions$) : of([]); } export function checkHasActions(cellButtonActions: TableCellButtonActionDescriptor[]): boolean { @@ -348,7 +357,7 @@ function filterTableCellButtonAction(widgetContext: WidgetContext, action: TableCellButtonActionDescriptor, data: EntityData | AlarmDataInfo | FormattedData): boolean { if (action.useShowActionCellButtonFunction) { try { - return action.showActionCellButtonFunction(widgetContext, data); + return action.showActionCellButtonFunction.execute(widgetContext, data); } catch (e) { console.warn('Failed to execute showActionCellButtonFunction', e); return false; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 7c232eb071..0e83f81961 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -58,7 +58,17 @@ import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/s import { CollectionViewer, DataSource } from '@angular/cdk/collections'; import { BehaviorSubject, fromEvent, merge, Observable, of, Subject, Subscription } from 'rxjs'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; -import { catchError, debounceTime, distinctUntilChanged, map, skip, startWith, takeUntil } from 'rxjs/operators'; +import { + catchError, + debounceTime, + distinctUntilChanged, + map, + skip, + startWith, + switchMap, + takeUntil, + tap +} from 'rxjs/operators'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @@ -828,7 +838,8 @@ class TimeseriesDatasource implements DataSource { private reserveSpaceForHiddenAction = true; private cellButtonActions: TableCellButtonActionDescriptor[]; - private readonly usedShowCellActionFunction: boolean; + private usedShowCellActionFunction: boolean; + private inited = false; constructor( private source: TimeseriesTableSource, @@ -837,14 +848,25 @@ class TimeseriesDatasource implements DataSource { private datePipe: DatePipe, private widgetContext: WidgetContext ) { - this.cellButtonActions = getTableCellButtonActions(widgetContext); - this.usedShowCellActionFunction = this.cellButtonActions.some(action => action.useShowActionCellButtonFunction); if (this.widgetContext.settings.reserveSpaceForHiddenAction) { this.reserveSpaceForHiddenAction = coerceBooleanProperty(this.widgetContext.settings.reserveSpaceForHiddenAction); } this.source.timeseriesDatasource = this; } + private init(): Observable { + if (this.inited) { + return of(null); + } + return getTableCellButtonActions(this.widgetContext).pipe( + tap(actions => { + this.cellButtonActions = actions + this.usedShowCellActionFunction = this.cellButtonActions.some(action => action.useShowActionCellButtonFunction); + this.inited = true; + }) + ); + } + connect(collectionViewer: CollectionViewer): Observable> { if (this.rowsSubject.isStopped) { this.rowsSubject.isStopped = false; @@ -886,68 +908,74 @@ class TimeseriesDatasource implements DataSource { } private updateSourceData() { - this.source.data = this.convertData(this.source.rawData, this.source.latestRawData); - this.allRowsSubject.next(this.source.data); - } - - private convertData(data: DatasourceData[], latestData: DatasourceData[]): TimeseriesRow[] { - const rowsMap: {[timestamp: number]: TimeseriesRow} = {}; - for (let d = 0; d < data.length; d++) { - const columnData = data[d].data; - columnData.forEach((cellData) => { - const timestamp = cellData[0]; - let row = rowsMap[timestamp]; - if (!row) { - row = { - formattedTs: this.datePipe.transform(timestamp, this.dateFormatFilter) - }; - if (this.cellButtonActions.length) { - if (this.usedShowCellActionFunction) { - const parsedData = formattedDataFormDatasourceData(data, undefined, timestamp); - row.actionCellButtons = prepareTableCellButtonActions(this.widgetContext, this.cellButtonActions, - parsedData[0], this.reserveSpaceForHiddenAction); - row.hasActions = checkHasActions(row.actionCellButtons); - } else { - row.hasActions = true; - row.actionCellButtons = this.cellButtonActions; + this.convertData(this.source.rawData, this.source.latestRawData).subscribe((data) => { + this.source.data = data; + this.allRowsSubject.next(this.source.data); + }); + } + + private convertData(data: DatasourceData[], latestData: DatasourceData[]): Observable { + return this.init().pipe( + map(() => { + const rowsMap: {[timestamp: number]: TimeseriesRow} = {}; + for (let d = 0; d < data.length; d++) { + const columnData = data[d].data; + columnData.forEach((cellData) => { + const timestamp = cellData[0]; + let row = rowsMap[timestamp]; + if (!row) { + row = { + formattedTs: this.datePipe.transform(timestamp, this.dateFormatFilter) + }; + if (this.cellButtonActions.length) { + if (this.usedShowCellActionFunction) { + const parsedData = formattedDataFormDatasourceData(data, undefined, timestamp); + row.actionCellButtons = prepareTableCellButtonActions(this.widgetContext, this.cellButtonActions, + parsedData[0], this.reserveSpaceForHiddenAction); + row.hasActions = checkHasActions(row.actionCellButtons); + } else { + row.hasActions = true; + row.actionCellButtons = this.cellButtonActions; + } + } + row[0] = timestamp; + for (let c = 0; c < (data.length + latestData.length); c++) { + row[c + 1] = undefined; + } + rowsMap[timestamp] = row; } - } - row[0] = timestamp; - for (let c = 0; c < (data.length + latestData.length); c++) { - row[c + 1] = undefined; - } - rowsMap[timestamp] = row; + row[d + 1] = cellData[1]; + }); } - row[d + 1] = cellData[1]; - }); - } - let rows: TimeseriesRow[] = []; - if (this.hideEmptyLines) { - for (const t of Object.keys(rowsMap)) { - let hideLine = true; - for (let c = 0; (c < data.length) && hideLine; c++) { - if (rowsMap[t][c + 1]) { - hideLine = false; + let rows: TimeseriesRow[] = []; + if (this.hideEmptyLines) { + for (const t of Object.keys(rowsMap)) { + let hideLine = true; + for (let c = 0; (c < data.length) && hideLine; c++) { + if (rowsMap[t][c + 1]) { + hideLine = false; + } + } + if (!hideLine) { + rows.push(rowsMap[t]); + } } + } else { + rows = Object.keys(rowsMap).map(itm => rowsMap[itm]); } - if (!hideLine) { - rows.push(rowsMap[t]); + for (let d = 0; d < latestData.length; d++) { + const columnData = latestData[d].data; + if (columnData.length) { + const value = columnData[0][1]; + rows.forEach((row) => { + row[data.length + d + 1] = value; + }); + } } - } - } else { - rows = Object.keys(rowsMap).map(itm => rowsMap[itm]); - } - for (let d = 0; d < latestData.length; d++) { - const columnData = latestData[d].data; - if (columnData.length) { - const value = columnData[0][1]; - rows.forEach((row) => { - row[data.length + d + 1] = value; - }); - } - } - return rows; + return rows; + }) + ); } isEmpty(): Observable { @@ -963,19 +991,23 @@ class TimeseriesDatasource implements DataSource { } private fetchRows(pageLink: PageLink): Observable> { - return this.allRows$.pipe( - map((data) => { - const fetchData = pageLink.filterData(data); - if (this.cellButtonActions.length) { - let maxCellButtonAction: number; - if (this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction) { - maxCellButtonAction = Math.max(...fetchData.data.map(tsRow => tsRow.actionCellButtons.length)); - } else { - maxCellButtonAction = this.cellButtonActions.length; - } - this.countCellButtonAction = maxCellButtonAction; - } - return fetchData; + return this.init().pipe( + switchMap(() => { + return this.allRows$.pipe( + map((data) => { + const fetchData = pageLink.filterData(data); + if (this.cellButtonActions.length) { + let maxCellButtonAction: number; + if (this.usedShowCellActionFunction && !this.reserveSpaceForHiddenAction) { + maxCellButtonAction = Math.max(...fetchData.data.map(tsRow => tsRow.actionCellButtons.length)); + } else { + maxCellButtonAction = this.cellButtonActions.length; + } + this.countCellButtonAction = maxCellButtonAction; + } + return fetchData; + }) + ); }) ); } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index a9634c24a8..92fc7d7621 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -120,7 +120,8 @@ import { DASHBOARD_PAGE_COMPONENT_TOKEN } from '@home/components/tokens'; import { MODULES_MAP } from '@shared/models/constants'; import { IModulesMap } from '@modules/common/modules-map.models'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; -import { compileTbFunction, isNotEmptyTbFunction } from '@shared/models/js-function.models'; +import { CompiledTbFunction, compileTbFunction, isNotEmptyTbFunction } from '@shared/models/js-function.models'; +import { HttpClient } from '@angular/common/http'; @Component({ selector: 'tb-widget', @@ -209,7 +210,8 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, private mobileService: MobileService, private raf: RafService, private ngZone: NgZone, - private cd: ChangeDetectorRef) { + private cd: ChangeDetectorRef, + private http: HttpClient) { super(store); this.cssParser.testMode = false; } @@ -270,37 +272,49 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, }; this.widgetContext.customHeaderActions = []; + const headerActionsDescriptors = this.getActionDescriptors(widgetActionSources.headerButton.value); - headerActionsDescriptors.forEach((descriptor) => - { + + const customHeaderActions$ = headerActionsDescriptors.map((descriptor) => { let useShowWidgetHeaderActionFunction = descriptor.useShowWidgetActionFunction || false; - let showWidgetHeaderActionFunction: ShowWidgetHeaderActionFunction = null; - if (useShowWidgetHeaderActionFunction && isNotEmptyStr(descriptor.showWidgetActionFunction)) { - try { - showWidgetHeaderActionFunction = - new Function('widgetContext', 'data', descriptor.showWidgetActionFunction) as ShowWidgetHeaderActionFunction; - } catch (e) { - useShowWidgetHeaderActionFunction = false; - } + let showWidgetHeaderActionFunction$: Observable>; + if (useShowWidgetHeaderActionFunction && isNotEmptyTbFunction(descriptor.showWidgetActionFunction)) { + showWidgetHeaderActionFunction$ = compileTbFunction(this.http, descriptor.showWidgetActionFunction, 'widgetContext', 'data'); + } else { + showWidgetHeaderActionFunction$ = of(null); } - const headerAction: WidgetHeaderAction = { - name: descriptor.name, - displayName: descriptor.displayName, - icon: descriptor.icon, - descriptor, - useShowWidgetHeaderActionFunction, - showWidgetHeaderActionFunction, - onAction: $event => { - const entityInfo = this.getActiveEntityInfo(); - const entityId = entityInfo ? entityInfo.entityId : null; - const entityName = entityInfo ? entityInfo.entityName : null; - const entityLabel = entityInfo ? entityInfo.entityLabel : null; - this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); - } - }; - this.widgetContext.customHeaderActions.push(headerAction); + return showWidgetHeaderActionFunction$.pipe( + catchError(() => { return of(null) }), + map(showWidgetHeaderActionFunction => { + if (!showWidgetHeaderActionFunction) { + useShowWidgetHeaderActionFunction = false; + } + const headerAction: WidgetHeaderAction = { + name: descriptor.name, + displayName: descriptor.displayName, + icon: descriptor.icon, + descriptor, + useShowWidgetHeaderActionFunction, + showWidgetHeaderActionFunction, + onAction: $event => { + const entityInfo = this.getActiveEntityInfo(); + const entityId = entityInfo ? entityInfo.entityId : null; + const entityName = entityInfo ? entityInfo.entityName : null; + const entityLabel = entityInfo ? entityInfo.entityLabel : null; + this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); + } + }; + return headerAction; + }) + ); }); + if (customHeaderActions$.length) { + forkJoin(customHeaderActions$).subscribe((customHeaderActions) => { + this.widgetContext.customHeaderActions.push(...customHeaderActions); + }); + } + this.subscriptionContext = new WidgetSubscriptionContext(this.widgetContext.dashboard); this.subscriptionContext.timeService = this.timeService; this.subscriptionContext.deviceService = this.deviceService; @@ -1110,7 +1124,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, case WidgetActionType.custom: const customFunction = descriptor.customFunction; if (isNotEmptyTbFunction(customFunction)) { - compileTbFunction(this.widgetContext.http, customFunction, '$event', 'widgetContext', 'entityId', + compileTbFunction(this.http, customFunction, '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel').subscribe( { next: (compiled) => { @@ -1143,7 +1157,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, this.loadCustomActionResources(actionNamespace, customCss, customResources, descriptor).subscribe({ next: () => { if (isNotEmptyTbFunction(customPrettyFunction)) { - compileTbFunction(this.widgetContext.http, customPrettyFunction, '$event', 'widgetContext', 'entityId', + compileTbFunction(this.http, customPrettyFunction, '$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel').subscribe( { next: (compiled) => { diff --git a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts index 6658f0b8a4..12c00597d5 100644 --- a/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/dashboard-component.models.ts @@ -686,7 +686,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { private filterCustomHeaderAction(action: WidgetHeaderAction, data: FormattedData[]): boolean { if (action.useShowWidgetHeaderActionFunction) { try { - return action.showWidgetHeaderActionFunction(this.widgetContext, data); + return action.showWidgetHeaderActionFunction.execute(this.widgetContext, data); } catch (e) { console.warn('Failed to execute showWidgetHeaderActionFunction', e); return false; diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 971c7db6f7..03af49d6d2 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -105,6 +105,7 @@ import { UserId } from '@shared/models/id/user-id'; import { UserSettingsService } from '@core/http/user-settings.service'; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { UtilsService } from '@core/services/utils.service'; +import { CompiledTbFunction } from '@shared/models/js-function.models'; export interface IWidgetAction { name: string; @@ -118,7 +119,7 @@ export interface WidgetHeaderAction extends IWidgetAction { displayName: string; descriptor: WidgetActionDescriptor; useShowWidgetHeaderActionFunction: boolean; - showWidgetHeaderActionFunction: ShowWidgetHeaderActionFunction; + showWidgetHeaderActionFunction: CompiledTbFunction; } export interface WidgetAction extends IWidgetAction { diff --git a/ui-ngx/src/app/shared/components/js-func.component.ts b/ui-ngx/src/app/shared/components/js-func.component.ts index b3b8ffe9e6..647ab6113d 100644 --- a/ui-ngx/src/app/shared/components/js-func.component.ts +++ b/ui-ngx/src/app/shared/components/js-func.component.ts @@ -42,11 +42,12 @@ import { TbEditorCompleter } from '@shared/models/ace/completion.models'; import { beautifyJs } from '@shared/models/beautify.models'; import { ScriptLanguage } from '@shared/models/rule-node.models'; import { coerceBoolean } from '@shared/decorators/coercion'; -import { loadModulesCompleter, TbFunction } from '@shared/models/js-function.models'; +import { compileTbFunction, loadModulesCompleter, TbFunction } from '@shared/models/js-function.models'; import { TbPopoverService } from '@shared/components/popover.service'; import { JsFuncModulesComponent } from '@shared/components/js-func-modules.component'; import { HttpClient } from '@angular/common/http'; -import { Observable, of } from 'rxjs'; +import { map, Observable, of } from 'rxjs'; +import { catchError } from 'rxjs/operators'; @Component({ selector: 'tb-js-func', @@ -322,23 +323,29 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, ); } - validateOnSubmit(): void { + validateOnSubmit(): Observable { if (!this.disabled) { this.cleanupJsErrors(); - this.functionValid = this.validateJsFunc(); - if (!this.functionValid) { - this.propagateValue(this.modelValue); - this.cd.markForCheck(); - this.store.dispatch(new ActionNotificationShow( - { - message: this.validationError, - type: 'error', - target: this.toastTargetId, - verticalPosition: 'bottom', - horizontalPosition: 'left' - })); - this.errorShowed = true; - } + return this.validateJsFunc().pipe( + map((valid) => { + this.functionValid = valid; + if (!this.functionValid) { + this.propagateValue(this.modelValue); + this.cd.markForCheck(); + this.store.dispatch(new ActionNotificationShow( + { + message: this.validationError, + type: 'error', + target: this.toastTargetId, + verticalPosition: 'bottom', + horizontalPosition: 'left' + })); + this.errorShowed = true; + } + }) + ); + } else { + return of(null); } } @@ -347,83 +354,95 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, this.jsEditor?.focus(); } - private validateJsFunc(): boolean { - try { - const toValidate = new Function(this.functionArgsString, this.modelValue); - if (this.noValidate) { - return true; - } - if (this.validationArgs) { - let res: any; - let validationError: any; - for (const validationArg of this.validationArgs) { - try { - res = toValidate.apply(this, validationArg); - validationError = null; - break; - } catch (e) { - validationError = e; - } - } - if (validationError) { - throw validationError; + private validateJsFunc(): Observable { + let toCompile: TbFunction; + if (this.withModules && this.modules && Object.keys(this.modules).length) { + toCompile = { + body: this.modelValue, + modules: this.modules + }; + } else { + toCompile = this.modelValue; + } + const args = this.functionArgs || []; + return compileTbFunction(this.http, toCompile, ...args).pipe( + map(toValidate => { + if (this.noValidate) { + return true; } - if (this.resultType !== 'nocheck') { - if (this.resultType === 'any') { - if (isUndefined(res)) { - this.validationError = this.translate.instant('js-func.no-return-error'); - return false; + if (this.validationArgs) { + let res: any; + let validationError: any; + for (const validationArg of this.validationArgs) { + try { + res = toValidate.apply(this, validationArg); + validationError = null; + break; + } catch (e) { + validationError = e; } - } else { - const resType = typeof res; - if (resType !== this.resultType) { - this.validationError = this.translate.instant('js-func.return-type-mismatch', {type: this.resultType}); - return false; + } + if (validationError) { + throw validationError; + } + if (this.resultType !== 'nocheck') { + if (this.resultType === 'any') { + if (isUndefined(res)) { + this.validationError = this.translate.instant('js-func.no-return-error'); + return false; + } + } else { + const resType = typeof res; + if (resType !== this.resultType) { + this.validationError = this.translate.instant('js-func.return-type-mismatch', {type: this.resultType}); + return false; + } } } + return true; + } else { + return true; } - return true; - } else { - return true; - } - } catch (e) { - const details = this.utils.parseException(e); - let errorInfo = 'Error:'; - if (details.name) { - errorInfo += ' ' + details.name + ':'; - } - if (details.message) { - errorInfo += ' ' + details.message; - } - if (details.lineNumber) { - errorInfo += '
Line ' + details.lineNumber; - if (details.columnNumber) { - errorInfo += ' column ' + details.columnNumber; + }), + catchError((e) => { + const details = this.utils.parseException(e); + let errorInfo = 'Error:'; + if (details.name) { + errorInfo += ' ' + details.name + ':'; } - errorInfo += ' of script.'; - } - this.validationError = errorInfo; - if (details.lineNumber) { - const line = details.lineNumber - 1; - let column = 0; - if (details.columnNumber) { - column = details.columnNumber; + if (details.message) { + errorInfo += ' ' + details.message; } - const errorMarkerId = this.jsEditor.session.addMarker(new Range(line, 0, line, Infinity), - 'ace_active-line', 'screenLine'); - this.errorMarkers.push(errorMarkerId); - const annotations = this.jsEditor.session.getAnnotations(); - const errorAnnotation: Ace.Annotation = { - row: line, - column, - text: details.message, - type: 'error' - }; - this.errorAnnotationId = annotations.push(errorAnnotation) - 1; - this.jsEditor.session.setAnnotations(annotations); - } - return false; - } + if (details.lineNumber) { + errorInfo += '
Line ' + details.lineNumber; + if (details.columnNumber) { + errorInfo += ' column ' + details.columnNumber; + } + errorInfo += ' of script.'; + } + this.validationError = errorInfo; + if (details.lineNumber) { + const line = details.lineNumber - 1; + let column = 0; + if (details.columnNumber) { + column = details.columnNumber; + } + const errorMarkerId = this.jsEditor.session.addMarker(new Range(line, 0, line, Infinity), + 'ace_active-line', 'screenLine'); + this.errorMarkers.push(errorMarkerId); + const annotations = this.jsEditor.session.getAnnotations(); + const errorAnnotation: Ace.Annotation = { + row: line, + column, + text: details.message, + type: 'error' + }; + this.errorAnnotationId = annotations.push(errorAnnotation) - 1; + this.jsEditor.session.setAnnotations(annotations); + } + return of(false); + }) + ); } private cleanupJsErrors(): void { @@ -449,6 +468,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, writeValue(value: TbFunction): void { if (isUndefinedOrNull(value) || typeof value === 'string') { this.modelValue = value as any; + this.modules = null; } else { this.modelValue = value.body; this.modules = value.modules; diff --git a/ui-ngx/src/app/shared/models/js-function.models.ts b/ui-ngx/src/app/shared/models/js-function.models.ts index 1b75111f5f..474796b74b 100644 --- a/ui-ngx/src/app/shared/models/js-function.models.ts +++ b/ui-ngx/src/app/shared/models/js-function.models.ts @@ -39,11 +39,11 @@ export const isNotEmptyTbFunction = (tbFunction: TbFunction): boolean => { return tbFunction.body && tbFunction.body.trim().length > 0; } } else { - return true; + return false; } } -export const compileTbFunction = (http: HttpClient, tbFunction: TbFunction, ...args: string[]): Observable => { +export const compileTbFunction = (http: HttpClient, tbFunction: TbFunction, ...args: string[]): Observable> => { let functionBody: string; let functionArgs: string[]; let modules: {[alias: string]: string }; @@ -59,7 +59,7 @@ export const compileTbFunction = (http: HttpClient, tbFunction: TbFunction, ...a return loadFunctionModules(http, modules).pipe( map((compiledModules) => { const compiledFunction = new Function(...functionArgs, functionBody); - return new CompiledTbFunction(compiledFunction, compiledModules); + return new CompiledTbFunction(compiledFunction, compiledModules); }) ); } @@ -188,13 +188,17 @@ const loadModuleCompletion = (http: HttpClient, moduleLink: string): Observable< ); } -export class CompiledTbFunction { +type GenericFunction = (...args: any[]) => any; + +export class CompiledTbFunction { + + public execute: T = this.executeImpl.bind(this); constructor(private compiledFunction: Function, private compiledModules: System.Module[]) { } - execute(...args: any[]): any { + private executeImpl(...args: any[]): any { let functionArgs: any[]; if (this.compiledModules?.length) { functionArgs = args ? args.concat(this.compiledModules) : this.compiledModules; @@ -292,8 +296,15 @@ const loadFunctionModuleSource = (http: HttpClient, moduleLink: string): Observa const getFunctionArguments = (func: Function): string[] => { const fnStr = func.toString().replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, ''); - let result = new Array(...fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(/([^\s,]+)/g)); - if (result === null) - result = []; - return result; + const firstBracketIndex = fnStr.indexOf('('); + const secondBracketIndex = fnStr.indexOf(')'); + if (firstBracketIndex === -1 || secondBracketIndex === -1 || (secondBracketIndex - firstBracketIndex) <= 1) { + return []; + } + const match = fnStr.slice(firstBracketIndex+1, secondBracketIndex).match(/([^\s,]+)/g); + if (match) { + return new Array(...match); + } else { + return []; + } } diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 718262dd53..3c91c4dd1f 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -335,7 +335,7 @@ export interface KeyInfo { label?: string; color?: string; funcBody?: string; - postFuncBody?: string; + postFuncBody?: TbFunction; units?: string; decimals?: number; } @@ -694,7 +694,7 @@ export interface WidgetActionDescriptor extends WidgetAction { icon: string; displayName?: string; useShowWidgetActionFunction?: boolean; - showWidgetActionFunction?: string; + showWidgetActionFunction?: TbFunction; columnIndex?: number; } From 3f50c3bd32c4527cc657304b348cdd77b491f92e Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 29 Nov 2024 18:35:39 +0200 Subject: [PATCH 61/77] UI: Improved formatValue function in utils --- ui-ngx/src/app/core/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 51f0f0eeb8..102e312d05 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -134,9 +134,9 @@ export function isLiteralObject(value: any) { export const formatValue = (value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined => { if (isDefinedAndNotNull(value) && isNumeric(value) && (isDefinedAndNotNull(dec) || isNotEmptyStr(units) || Number(value).toString() === value)) { - let formatted: string | number = isDefinedAndNotNull(dec) ? Number(value) : (value as number); + let formatted = value; if (isDefinedAndNotNull(dec)) { - formatted = formatted.toFixed(dec); + formatted = Number(formatted).toFixed(dec); } if (!showZeroDecimals) { formatted = (Number(formatted)); From 5d559616ae59996feaadfacec8bed3d861667eff Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 29 Nov 2024 18:44:21 +0200 Subject: [PATCH 62/77] UI: Change translation --- ui-ngx/src/assets/locale/locale.constant-en_US.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 6f288f3db5..676c0ce0ce 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -524,11 +524,11 @@ "signings-key-base64": "Signing key must be base64 format.", "expiration-time": "Token expiration time (sec)", "expiration-time-required": "Token expiration time is required.", - "expiration-time-max": "Maximal time is 2147483647 seconds (68 years).", + "expiration-time-max": "Maximum allowed time is 2147483647 seconds(68 years).", "expiration-time-min": "Minimum time is 60 seconds (1 minute).", "refresh-expiration-time": "Refresh token expiration time (sec)", "refresh-expiration-time-required": "Refresh token expiration time is required.", - "refresh-expiration-time-max": "Maximal time is 2147483647 seconds (68 years).", + "refresh-expiration-time-max": "Maximum allowed time is 2147483647 seconds (68 years).", "refresh-expiration-time-min": "Minimum time is 900 seconds (15 minute).", "refresh-expiration-time-less-token": "Refresh token time must be greater token time.", "generate-key": "Generate key", From 3c59c9b991726377deca0e2dc8870eea4e7952a6 Mon Sep 17 00:00:00 2001 From: mpetrov Date: Fri, 29 Nov 2024 19:36:11 +0200 Subject: [PATCH 63/77] disable timer on allEnabled=true --- .../debug-settings-button.component.ts | 17 ++++++++++---- .../debug-settings-panel.component.ts | 23 +++++++++++-------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-button.component.ts b/ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-button.component.ts index 292f11e6d7..b98c87edf9 100644 --- a/ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-button.component.ts +++ b/ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-button.component.ts @@ -30,10 +30,10 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { MatButton } from '@angular/material/button'; import { DebugSettingsPanelComponent } from './debug-settings-panel.component'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { shareReplay, timer } from 'rxjs'; +import { of, shareReplay, timer } from 'rxjs'; import { SECOND } from '@shared/models/time/time.models'; import { DebugSettings } from '@shared/models/entity.models'; -import { map } from 'rxjs/operators'; +import { map, startWith, switchMap } from 'rxjs/operators'; import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { AppState } from '@core/core.state'; import { Store } from '@ngrx/store'; @@ -70,9 +70,18 @@ export class DebugSettingsButtonComponent implements ControlValueAccessor { allEnabled: [false], allEnabledUntil: [] }); - disabled = false; - isDebugAllActive$ = timer(0, SECOND).pipe(map(() => this.allEnabledUntil > new Date().getTime() || this.allEnabled), shareReplay(1)); + isDebugAllActive$ = this.debugSettingsFormGroup.get('allEnabled').valueChanges.pipe( + startWith(this.debugSettingsFormGroup.get('allEnabled').value), + switchMap(value => { + if (value) { + return of(true); + } else { + return timer(0, SECOND).pipe(map(() => this.allEnabledUntil > new Date().getTime())); + } + }), + shareReplay(1) + ); readonly maxDebugModeDurationMinutes = getCurrentAuthState(this.store).maxDebugModeDurationMinutes; diff --git a/ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-panel.component.ts index b4c71fab5e..ff19a0bddb 100644 --- a/ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-panel.component.ts @@ -30,10 +30,10 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { MINUTE, SECOND } from '@shared/models/time/time.models'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; -import { shareReplay, timer } from 'rxjs'; +import { of, shareReplay, timer } from 'rxjs'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { DebugSettings } from '@shared/models/entity.models'; -import { distinctUntilChanged, map, tap } from 'rxjs/operators'; +import { map, startWith, switchMap, tap } from 'rxjs/operators'; @Component({ selector: 'tb-debug-settings-panel', @@ -61,12 +61,15 @@ export class DebugSettingsPanelComponent extends PageComponent implements OnInit maxMessagesCount: string; maxTimeFrameSec: string; - isDebugAllActive$ = timer(0, SECOND).pipe( - map(() => { - this.cd.markForCheck(); - return this.allEnabledUntil > new Date().getTime() || this.allEnabled; + isDebugAllActive$ = this.debugAllControl.valueChanges.pipe( + startWith(this.debugAllControl.value), + switchMap(value => { + if (value) { + return of(true); + } else { + return timer(0, SECOND).pipe(map(() => this.allEnabledUntil > new Date().getTime())); + } }), - distinctUntilChanged(), tap(isDebugOn => this.debugAllControl.patchValue(isDebugOn, { emitEvent: false })), shareReplay(1), ); @@ -91,15 +94,15 @@ export class DebugSettingsPanelComponent extends PageComponent implements OnInit onApply(): void { this.onConfigApplied.emit({ - allEnabled: this.allEnabled, + allEnabled: this.debugAllControl.value, failuresEnabled: this.onFailuresControl.value, allEnabledUntil: this.allEnabledUntil }); } onReset(): void { - this.allEnabled = true; - this.allEnabledUntil = new Date().getTime() + this.maxDebugModeDurationMinutes * MINUTE; + this.debugAllControl.patchValue(true); + this.allEnabledUntil = 0; this.cd.markForCheck(); } From 008ac7970e971c9e4230a2bad9e1b8b57973cf23 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 2 Dec 2024 11:21:20 +0200 Subject: [PATCH 64/77] UI: Clean up timer memory lick and refactoring debug setting components --- .../debug-settings-button.component.html | 2 +- .../debug-settings-button.component.ts | 34 +++++++--------- .../debug-settings-panel.component.html | 4 +- .../debug-settings-panel.component.ts | 40 ++++++++++--------- .../components/event/event-table-config.ts | 3 +- ...enant-profile-configuration.component.html | 2 +- .../rule-node-details.component.html | 2 +- 7 files changed, 44 insertions(+), 43 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-button.component.html b/ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-button.component.html index 11c396add1..b779ae1e4c 100644 --- a/ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-button.component.html +++ b/ui-ngx/src/app/modules/home/components/debug-settings/debug-settings-button.component.html @@ -16,7 +16,7 @@ -->