From 9f9319b791636651863f7c7b00d55c5c935f53c4 Mon Sep 17 00:00:00 2001 From: yevhenii Date: Thu, 6 Mar 2025 18:07:20 +0200 Subject: [PATCH 01/54] Fix RuleChainMetadata for older Edge versions - Check for Edge version and removal of data introduced in later versions. --- .../service/edge/EdgeMsgConstructorUtils.java | 38 +++++++++++++++++-- .../service/edge/rpc/EdgeGrpcSession.java | 2 +- .../edge/rpc/processor/EdgeProcessor.java | 3 +- .../processor/alarm/AlarmEdgeProcessor.java | 3 +- .../comment/AlarmCommentEdgeProcessor.java | 3 +- .../processor/asset/AssetEdgeProcessor.java | 3 +- .../profile/AssetProfileEdgeProcessor.java | 3 +- .../customer/CustomerEdgeProcessor.java | 3 +- .../dashboard/DashboardEdgeProcessor.java | 3 +- .../processor/device/DeviceEdgeProcessor.java | 3 +- .../profile/DeviceProfileEdgeProcessor.java | 3 +- .../processor/edge/EdgeEntityProcessor.java | 3 +- .../entityview/EntityViewEdgeProcessor.java | 3 +- .../NotificationRuleEdgeProcessor.java | 3 +- .../NotificationTargetEdgeProcessor.java | 3 +- .../NotificationTemplateEdgeProcessor.java | 3 +- .../processor/oauth2/DomainEdgeProcessor.java | 3 +- .../oauth2/OAuth2ClientEdgeProcessor.java | 3 +- .../ota/OtaPackageEdgeProcessor.java | 3 +- .../processor/queue/QueueEdgeProcessor.java | 3 +- .../relation/RelationEdgeProcessor.java | 4 +- .../resource/ResourceEdgeProcessor.java | 3 +- .../rule/RuleChainEdgeProcessor.java | 5 ++- .../rule/RuleChainMetadataEdgeProcessor.java | 5 ++- .../settings/AdminSettingsEdgeProcessor.java | 3 +- .../processor/tenant/TenantEdgeProcessor.java | 3 +- .../tenant/TenantProfileEdgeProcessor.java | 3 +- .../rpc/processor/user/UserEdgeProcessor.java | 3 +- .../widget/WidgetBundleEdgeProcessor.java | 3 +- .../widget/WidgetTypeEdgeProcessor.java | 3 +- 30 files changed, 94 insertions(+), 35 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index b59dc096ee..703d42abf5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.edge; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -24,6 +25,7 @@ import com.google.gson.JsonPrimitive; import com.google.gson.reflect.TypeToken; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; @@ -89,6 +91,7 @@ import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EntityDataProto; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; @@ -417,8 +420,37 @@ public class EdgeMsgConstructorUtils { .setIdLSB(ruleChainId.getId().getLeastSignificantBits()).build(); } - public static RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData) { - return RuleChainMetadataUpdateMsg.newBuilder().setMsgType(msgType).setEntity(JacksonUtil.toString(ruleChainMetaData)).build(); + public static RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { + String metaData = prepareMetaDataForEdgeVersion(ruleChainMetaData, edgeVersion); + + return RuleChainMetadataUpdateMsg.newBuilder() + .setMsgType(msgType) + .setEntity(metaData) + .build(); + } + + private static String prepareMetaDataForEdgeVersion(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { + if (edgeVersion == EdgeVersion.V_3_7_0 || edgeVersion == EdgeVersion.V_3_8_0) { + JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); + JsonNode nodes = jsonNode.get("nodes"); + + for (JsonNode node : nodes) { + if (node.isObject()) + prepareRuleNodeForOldEdgeVersion((ObjectNode) node); + } + return JacksonUtil.toString(jsonNode); + } else { + return JacksonUtil.toString(ruleChainMetaData); + } + } + + private static void prepareRuleNodeForOldEdgeVersion(ObjectNode node) { + if (TbMsgTimeseriesNode.class.getName().equals(node.get("type").asText())) { + JsonNode configurationNode = node.get("configuration"); + if (configurationNode != null && configurationNode.isObject()) { + ((ObjectNode) configurationNode).remove("processingSettings"); + } + } } public static EntityDataProto constructEntityDataMsg(TenantId tenantId, EntityId entityId, EdgeEventActionType actionType, JsonElement entityData) { @@ -467,7 +499,7 @@ public class EdgeMsgConstructorUtils { AttributeDeleteMsg.Builder attributeDeleteMsg = AttributeDeleteMsg.newBuilder(); attributeDeleteMsg.setScope(entityData.getAsJsonObject().getAsJsonPrimitive("scope").getAsString()); JsonArray jsonArray = entityData.getAsJsonObject().getAsJsonArray("keys"); - List keys = new Gson().fromJson(jsonArray.toString(), new TypeToken<>(){}.getType()); + List keys = new Gson().fromJson(jsonArray.toString(), new TypeToken<>() {}.getType()); attributeDeleteMsg.addAllAttributeNames(keys); attributeDeleteMsg.build(); builder.setAttributeDeleteMsg(attributeDeleteMsg); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index a970097548..ecd0c6d9f3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -780,7 +780,7 @@ public abstract class EdgeGrpcSession implements Closeable { return null; } - return ctx.getProcessor(edgeEvent.getType()).convertEdgeEventToDownlink(edgeEvent); + return ctx.getProcessor(edgeEvent.getType()).convertEdgeEventToDownlink(edgeEvent, edgeVersion); } public void addEventToHighPriorityQueue(EdgeEvent edgeEvent) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java index 0f17949437..dd6221c62b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/EdgeProcessor.java @@ -20,13 +20,14 @@ import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.transport.TransportProtos; public interface EdgeProcessor { ListenableFuture processEntityNotification(TenantId tenantId, TransportProtos.EdgeNotificationMsgProto msg); - default DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + default DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { return null; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java index df9af20f84..6c4cbe7a94 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/AlarmEdgeProcessor.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -55,7 +56,7 @@ public class AlarmEdgeProcessor extends BaseAlarmProcessor implements AlarmProce } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { AlarmUpdateMsg alarmUpdateMsg = convertAlarmEventToAlarmMsg(edgeEvent); if (alarmUpdateMsg != null) { return DownlinkMsg.newBuilder() diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/comment/AlarmCommentEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/comment/AlarmCommentEdgeProcessor.java index 449f4867d1..4a76a3527b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/comment/AlarmCommentEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/comment/AlarmCommentEdgeProcessor.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -57,7 +58,7 @@ public class AlarmCommentEdgeProcessor extends BaseAlarmProcessor implements Ala } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); switch (edgeEvent.getAction()) { case ADDED_COMMENT: diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index e7a9720419..47f4c11362 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -107,7 +108,7 @@ public class AssetEdgeProcessor extends BaseAssetProcessor implements AssetProce } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { AssetId assetId = new AssetId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java index 7a0ae0ccc6..e05b6f41c3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -94,7 +95,7 @@ public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor impleme } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java index 40cbe3985f..784d10b26c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/customer/CustomerEdgeProcessor.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -48,7 +49,7 @@ import java.util.UUID; public class CustomerEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { CustomerId customerId = new CustomerId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java index f9a7311f31..e1259a7e0e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -100,7 +101,7 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor implements Da } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { DashboardId dashboardId = new DashboardId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index 08f6b294bf..ab01f83cd8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -49,6 +49,7 @@ import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; @@ -222,7 +223,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor implements DevicePr } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED: diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java index 35857d8edb..3355bc819f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -94,7 +95,7 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor imple } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java index 77fa31c028..5d4674ba1f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/edge/EdgeEntityProcessor.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -101,7 +102,7 @@ public class EdgeEntityProcessor extends BaseEdgeProcessor { } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { EdgeId edgeId = new EdgeId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java index a8844e7397..56785f9ac0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -106,7 +107,7 @@ public class EntityViewEdgeProcessor extends BaseEntityViewProcessor implements } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { EntityViewId entityViewId = new EntityViewId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED, ASSIGNED_TO_EDGE, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationRuleEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationRuleEdgeProcessor.java index 77f97e0775..918ca024cd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationRuleEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationRuleEdgeProcessor.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationRuleId; import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.dao.notification.NotificationRuleService; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.NotificationRuleUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -40,7 +41,7 @@ public class NotificationRuleEdgeProcessor extends BaseEdgeProcessor { private NotificationRuleService notificationRuleService; @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { NotificationRuleId notificationRuleId = new NotificationRuleId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTargetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTargetEdgeProcessor.java index 5dfad971b0..0f95248c74 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTargetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTargetEdgeProcessor.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationTargetId; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.dao.notification.NotificationTargetService; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.NotificationTargetUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -40,7 +41,7 @@ public class NotificationTargetEdgeProcessor extends BaseEdgeProcessor { private NotificationTargetService notificationTargetService; @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { NotificationTargetId notificationTargetId = new NotificationTargetId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTemplateEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTemplateEdgeProcessor.java index b79f4ab4b8..f3ae939514 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTemplateEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/notification/NotificationTemplateEdgeProcessor.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.NotificationTemplateId; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.dao.notification.NotificationTemplateService; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.NotificationTemplateUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -40,7 +41,7 @@ public class NotificationTemplateEdgeProcessor extends BaseEdgeProcessor { private NotificationTemplateService notificationTemplateService; @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { NotificationTemplateId notificationTemplateId = new NotificationTemplateId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/DomainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/DomainEdgeProcessor.java index 6b3d429a2f..07247d946c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/DomainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/DomainEdgeProcessor.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.id.DomainId; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; import org.thingsboard.server.gen.edge.v1.OAuth2DomainUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -42,7 +43,7 @@ public class DomainEdgeProcessor extends BaseEdgeProcessor { private DomainService domainService; @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { DomainId domainId = new DomainId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2ClientEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2ClientEdgeProcessor.java index 20bd5b3f7e..08b0405702 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2ClientEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/oauth2/OAuth2ClientEdgeProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.OAuth2ClientId; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.OAuth2ClientUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class OAuth2ClientEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { OAuth2ClientId oAuth2ClientId = new OAuth2ClientId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java index b2080e8176..e22ed3bafa 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ota/OtaPackageEdgeProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.OtaPackageUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class OtaPackageEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { OtaPackageId otaPackageId = new OtaPackageId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java index ebea0c8255..f5fcac3342 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/queue/QueueEdgeProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class QueueEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { QueueId queueId = new QueueId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java index da6f749269..70666e8b0e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/relation/RelationEdgeProcessor.java @@ -21,7 +21,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -30,6 +29,7 @@ import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RelationUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.transport.TransportProtos; @@ -85,7 +85,7 @@ public class RelationEdgeProcessor extends BaseRelationProcessor implements Rela } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { EntityRelation entityRelation = JacksonUtil.convertValue(edgeEvent.getBody(), EntityRelation.class); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); RelationUpdateMsg relationUpdateMsg = EdgeMsgConstructorUtils.constructRelationUpdatedMsg(msgType, entityRelation); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java index 8d2c06025a..de31bf6a9c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -73,7 +74,7 @@ public class ResourceEdgeProcessor extends BaseResourceProcessor implements Reso } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { TbResourceId tbResourceId = new TbResourceId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index 06fb4c37a2..b949660e70 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -134,7 +135,7 @@ public class RuleChainEdgeProcessor extends BaseRuleChainProcessor { } @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); DownlinkMsg downlinkMsg = null; switch (edgeEvent.getAction()) { @@ -161,7 +162,7 @@ public class RuleChainEdgeProcessor extends BaseRuleChainProcessor { RuleChainMetaData ruleChainMetaData = edgeCtx.getRuleChainService().loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = EdgeMsgConstructorUtils - .constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData); + .constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData, edgeVersion); builder.addRuleChainMetadataUpdateMsg(ruleChainMetadataUpdateMsg); downlinkMsg = builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainMetadataEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainMetadataEdgeProcessor.java index fe9a66c456..845d2a7204 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainMetadataEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainMetadataEdgeProcessor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -36,14 +37,14 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class RuleChainMetadataEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { RuleChainId ruleChainId = new RuleChainId(edgeEvent.getEntityId()); RuleChain ruleChain = edgeCtx.getRuleChainService().findRuleChainById(edgeEvent.getTenantId(), ruleChainId); if (ruleChain != null) { RuleChainMetaData ruleChainMetaData = edgeCtx.getRuleChainService().loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = EdgeMsgConstructorUtils - .constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData); + .constructRuleChainMetadataUpdatedMsg(msgType, ruleChainMetaData, edgeVersion); return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addRuleChainMetadataUpdateMsg(ruleChainMetadataUpdateMsg) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java index 6028273dc2..f2f82fe9de 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/settings/AdminSettingsEdgeProcessor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; @@ -33,7 +34,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class AdminSettingsEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { AdminSettings adminSettings = JacksonUtil.convertValue(edgeEvent.getBody(), AdminSettings.class); if (adminSettings == null) { return null; 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 e21ed0b4ea..f0f41b5cc6 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 @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; @@ -38,7 +39,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class TenantEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { TenantId tenantId = TenantId.fromUUID(edgeEvent.getEntityId()); if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) { Tenant tenant = edgeCtx.getTenantService().findTenantById(tenantId); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java index bcbeeb82b3..a0bb32503e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/tenant/TenantProfileEdgeProcessor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -36,7 +37,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class TenantProfileEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { TenantProfileId tenantProfileId = new TenantProfileId(edgeEvent.getEntityId()); if (EdgeEventActionType.UPDATED.equals(edgeEvent.getAction())) { TenantProfile tenantProfile = edgeCtx.getTenantProfileService().findTenantProfileById(edgeEvent.getTenantId(), tenantProfileId); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index c8c0b49124..fdd03d63f3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -36,7 +37,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class UserEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { UserId userId = new UserId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java index 4c22ddf898..52d7370470 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetBundleEdgeProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.WidgetsBundleId; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -37,7 +38,7 @@ import java.util.List; public class WidgetBundleEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java index 4da508b22b..47f30afa93 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/widget/WidgetTypeEdgeProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -35,7 +36,7 @@ import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; public class WidgetTypeEdgeProcessor extends BaseEdgeProcessor { @Override - public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent) { + public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { WidgetTypeId widgetTypeId = new WidgetTypeId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { case ADDED, UPDATED -> { From ab4ed8fa7489c3225e18f6b5d56859b270fb7dbc Mon Sep 17 00:00:00 2001 From: yevhenii Date: Thu, 6 Mar 2025 18:14:15 +0200 Subject: [PATCH 02/54] Fix RuleChainMetadata for older Edge versions - refactoring --- .../server/service/edge/EdgeMsgConstructorUtils.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 703d42abf5..e0974df8f8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -115,6 +115,7 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.List; import java.util.UUID; @@ -430,7 +431,7 @@ public class EdgeMsgConstructorUtils { } private static String prepareMetaDataForEdgeVersion(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { - if (edgeVersion == EdgeVersion.V_3_7_0 || edgeVersion == EdgeVersion.V_3_8_0) { + if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0)) { JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); JsonNode nodes = jsonNode.get("nodes"); From c4a629fc9b5c5227af17ee9bfdede0374c871673 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Fri, 7 Mar 2025 13:26:54 +0200 Subject: [PATCH 03/54] Fix RuleChainMetadata for older Edge versions - fixed EdgeVersion --- .../server/service/edge/EdgeMsgConstructorUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index e0974df8f8..d3e8e3104e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -431,7 +431,7 @@ public class EdgeMsgConstructorUtils { } private static String prepareMetaDataForEdgeVersion(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { - if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0)) { + if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0)) { JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); JsonNode nodes = jsonNode.get("nodes"); From 80a7edc8f784465765ba710d1ab201187133e5f4 Mon Sep 17 00:00:00 2001 From: yevhenii Date: Tue, 11 Mar 2025 17:58:27 +0200 Subject: [PATCH 04/54] Fix RuleChainMetadata for older Edge versions - added test - refactoring --- .../service/edge/EdgeMsgConstructorUtils.java | 16 ++-- .../edge/EdgeMsgConstructorUtilsTest.java | 91 +++++++++++++++++++ 2 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index d3e8e3104e..11e3172168 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -422,7 +422,7 @@ public class EdgeMsgConstructorUtils { } public static RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { - String metaData = prepareMetaDataForEdgeVersion(ruleChainMetaData, edgeVersion); + String metaData = filterMetadataForOldEdgeVersions(ruleChainMetaData, edgeVersion); return RuleChainMetadataUpdateMsg.newBuilder() .setMsgType(msgType) @@ -430,14 +430,15 @@ public class EdgeMsgConstructorUtils { .build(); } - private static String prepareMetaDataForEdgeVersion(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { + private static String filterMetadataForOldEdgeVersions(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0)) { JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); JsonNode nodes = jsonNode.get("nodes"); for (JsonNode node : nodes) { - if (node.isObject()) - prepareRuleNodeForOldEdgeVersion((ObjectNode) node); + if (node.isObject()) { + removeIncompatibleFields((ObjectNode) node); + } } return JacksonUtil.toString(jsonNode); } else { @@ -445,11 +446,10 @@ public class EdgeMsgConstructorUtils { } } - private static void prepareRuleNodeForOldEdgeVersion(ObjectNode node) { + private static void removeIncompatibleFields(ObjectNode node) { if (TbMsgTimeseriesNode.class.getName().equals(node.get("type").asText())) { - JsonNode configurationNode = node.get("configuration"); - if (configurationNode != null && configurationNode.isObject()) { - ((ObjectNode) configurationNode).remove("processingSettings"); + if (node.has("configuration") && node.get("configuration").isObject()) { + ((ObjectNode) node.get("configuration")).remove("processingSettings"); } } } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java new file mode 100644 index 0000000000..5e7cfcfc28 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.edge; + +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.gen.edge.v1.EdgeVersion; +import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; + +import java.util.Collections; + +public class EdgeMsgConstructorUtilsTest { + private static final int CONFIGURATION_VERSION = 5; + + @Test + public void testRuleChainMetadataUpdateMsgForAllEdgeVersions() { + // GIVEN + RuleChainMetaData metaData = createIncompatibleRuleNodesForOldEdge(); + + // WHEN + RuleNode ruleNode_V_4_0_0 = getRuleNodeFromMetadataUpdateMessage(metaData, EdgeVersion.V_4_0_0); + RuleNode ruleNode_V_3_9_0 = getRuleNodeFromMetadataUpdateMessage(metaData, EdgeVersion.V_3_9_0); + RuleNode ruleNode_V_3_8_0 = getRuleNodeFromMetadataUpdateMessage(metaData, EdgeVersion.V_3_8_0); + RuleNode ruleNode_V_3_7_0 = getRuleNodeFromMetadataUpdateMessage(metaData, EdgeVersion.V_3_7_0); + + // THEN + assertRuleNodeConfiguration(ruleNode_V_4_0_0, EdgeVersion.V_4_0_0); + assertRuleNodeConfiguration(ruleNode_V_3_9_0, EdgeVersion.V_3_9_0); + assertRuleNodeConfiguration(ruleNode_V_3_8_0, EdgeVersion.V_3_8_0); + assertRuleNodeConfiguration(ruleNode_V_3_7_0, EdgeVersion.V_3_7_0); + } + + private RuleChainMetaData createIncompatibleRuleNodesForOldEdge() { + RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + + RuleNode ruleNode1 = new RuleNode(); + ruleNode1.setName("TbMsgTimeseriesNode"); + ruleNode1.setType(org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode.class.getName()); + ruleNode1.setConfigurationVersion(CONFIGURATION_VERSION); + ruleNode1.setConfiguration(JacksonUtil.valueToTree(new TbMsgTimeseriesNodeConfiguration().defaultConfiguration())); + + ruleChainMetaData.setFirstNodeIndex(0); + ruleChainMetaData.setNodes(Collections.singletonList(ruleNode1)); + + return ruleChainMetaData; + } + + + private RuleNode getRuleNodeFromMetadataUpdateMessage(RuleChainMetaData metaData, EdgeVersion edgeVersion) { + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = + EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, metaData, edgeVersion); + + RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true); + Assert.assertNotNull("RuleChainMetaData is null", ruleChainMetaData); + + RuleNode ruleNode = ruleChainMetaData.getNodes().stream().findFirst().orElse(null); + Assert.assertNotNull("RuleNode is null for Edge version " + edgeVersion, ruleNode); + Assert.assertNotNull("Configuration is null for Edge version " + edgeVersion, ruleNode.getConfiguration()); + + return ruleNode; + } + + private void assertRuleNodeConfiguration(RuleNode ruleNode, EdgeVersion edgeVersion) { + if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0)) { + Assert.assertEquals("Unexpected config size", 2, ruleNode.getConfiguration().size()); + Assert.assertFalse("Unexpected field 'processingSettings'", ruleNode.getConfiguration().has("processingSettings")); + }else{ + Assert.assertEquals("Unexpected config size", 3, ruleNode.getConfiguration().size()); + Assert.assertTrue("Missing field 'processingSettings'", ruleNode.getConfiguration().has("processingSettings")); + } + } +} From 4e51398cc401415619b084ee382465c61d85da80 Mon Sep 17 00:00:00 2001 From: yevhenii Date: Tue, 18 Mar 2025 12:24:14 +0200 Subject: [PATCH 05/54] Fix RuleChainMetadata for older Edge versions - added new problem node - added test - refactoring --- .../service/edge/EdgeMsgConstructorUtils.java | 49 ++++-- .../edge/EdgeMsgConstructorUtilsTest.java | 146 +++++++++++++----- 2 files changed, 149 insertions(+), 46 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 11e3172168..61875f1277 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -25,6 +25,10 @@ import com.google.gson.JsonPrimitive; import com.google.gson.reflect.TypeToken; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode; +import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode; +import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode; +import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode; import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.Customer; @@ -117,11 +121,25 @@ import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; +import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.UUID; @Slf4j public class EdgeMsgConstructorUtils { + public static final Map NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION = Map.of( + TbMsgTimeseriesNode.class.getName(), "processingSettings", + TbMsgAttributesNode.class.getName(), "processingSettings", + TbSaveToCustomCassandraTableNode.class.getName(), "defaultTtl" + ); + + //added in edge version 3.8.0 + public static final Set MISSING_NODES_IN_VERSION_37 = Set.of( + TbSendRestApiCallReplyNode.class.getName(), + TbAwsLambdaNode.class.getName() + ); public static AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm) { return AlarmUpdateMsg.newBuilder().setMsgType(msgType) @@ -431,25 +449,36 @@ public class EdgeMsgConstructorUtils { } private static String filterMetadataForOldEdgeVersions(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { - if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0)) { - JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); - JsonNode nodes = jsonNode.get("nodes"); + JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); + JsonNode nodes = jsonNode.get("nodes"); + + if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0)) { + Iterator iterator = nodes.iterator(); + while (iterator.hasNext()) { + JsonNode node = iterator.next(); - for (JsonNode node : nodes) { - if (node.isObject()) { - removeIncompatibleFields((ObjectNode) node); + String type = node.get("type").asText(); + if (MISSING_NODES_IN_VERSION_37.contains(type)) { + iterator.remove(); } } + } + + if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0)) { + nodes.forEach(EdgeMsgConstructorUtils::changeRuleNodeConfigForOldEdgeVersion); + return JacksonUtil.toString(jsonNode); } else { return JacksonUtil.toString(ruleChainMetaData); } } - private static void removeIncompatibleFields(ObjectNode node) { - if (TbMsgTimeseriesNode.class.getName().equals(node.get("type").asText())) { - if (node.has("configuration") && node.get("configuration").isObject()) { - ((ObjectNode) node.get("configuration")).remove("processingSettings"); + private static void changeRuleNodeConfigForOldEdgeVersion(JsonNode node) { + if (node.isObject()) { + JsonNode configurationNode = node.get("configuration"); + if (configurationNode != null && configurationNode.isObject() && + NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION.containsKey(node.get("type").asText())) { + ((ObjectNode) configurationNode).remove(NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION.get(node.get("type").asText())); } } } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index 5e7cfcfc28..84ccfdb3ee 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -15,9 +15,20 @@ */ package org.thingsboard.server.service.edge; +import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode; +import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNodeConfiguration; +import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode; +import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNodeConfiguration; +import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode; +import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNodeConfiguration; +import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode; +import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNodeConfiguration; +import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; @@ -26,66 +37,129 @@ import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; -import java.util.Collections; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.MISSING_NODES_IN_VERSION_37; +import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION; + +@Slf4j public class EdgeMsgConstructorUtilsTest { private static final int CONFIGURATION_VERSION = 5; + public static final List TEST_SUPPORTED_EDGE_VERSIONS = Arrays.asList( + EdgeVersion.V_4_0_0, EdgeVersion.V_3_9_0, EdgeVersion.V_3_8_0, EdgeVersion.V_3_7_0 + ); + + private static final Map CONFIG_TO_NODE_NAME = Map.of( + new TbMsgTimeseriesNodeConfiguration(), TbMsgTimeseriesNode.class.getName(), + new TbMsgAttributesNodeConfiguration(), TbMsgAttributesNode.class.getName(), + new TbSaveToCustomCassandraTableNodeConfiguration(), TbSaveToCustomCassandraTableNode.class.getName() + ); + + private static final Map NODE_TO_CONFIG_PARAMS_COUNT = Map.of( + TbMsgTimeseriesNode.class.getName(), 3, + TbMsgAttributesNode.class.getName(), 5, + TbSaveToCustomCassandraTableNode.class.getName(), 3 + ); + + private static final Map CONFIG_TO_MISS_NODE_FOR_OLD_EDGE = Map.of( + new TbSendRestApiCallReplyNodeConfiguration(), TbSendRestApiCallReplyNode.class.getName(), + new TbAwsLambdaNodeConfiguration(), TbAwsLambdaNode.class.getName() + ); + @Test - public void testRuleChainMetadataUpdateMsgForAllEdgeVersions() { + public void testRuleChainMetadataUpdateMsgForOldEdgeVersions() { // GIVEN - RuleChainMetaData metaData = createIncompatibleRuleNodesForOldEdge(); - - // WHEN - RuleNode ruleNode_V_4_0_0 = getRuleNodeFromMetadataUpdateMessage(metaData, EdgeVersion.V_4_0_0); - RuleNode ruleNode_V_3_9_0 = getRuleNodeFromMetadataUpdateMessage(metaData, EdgeVersion.V_3_9_0); - RuleNode ruleNode_V_3_8_0 = getRuleNodeFromMetadataUpdateMessage(metaData, EdgeVersion.V_3_8_0); - RuleNode ruleNode_V_3_7_0 = getRuleNodeFromMetadataUpdateMessage(metaData, EdgeVersion.V_3_7_0); - - // THEN - assertRuleNodeConfiguration(ruleNode_V_4_0_0, EdgeVersion.V_4_0_0); - assertRuleNodeConfiguration(ruleNode_V_3_9_0, EdgeVersion.V_3_9_0); - assertRuleNodeConfiguration(ruleNode_V_3_8_0, EdgeVersion.V_3_8_0); - assertRuleNodeConfiguration(ruleNode_V_3_7_0, EdgeVersion.V_3_7_0); + RuleChainMetaData metaData = createMetadataWithProblemNodes(CONFIG_TO_NODE_NAME); + + TEST_SUPPORTED_EDGE_VERSIONS.forEach(edgeVersion -> { + // WHEN + List ruleNodes = getRuleNodesFromUpdateMsg(metaData, edgeVersion); + + // THEN + validateRuleNodeConfig(ruleNodes, edgeVersion); + }); } - private RuleChainMetaData createIncompatibleRuleNodesForOldEdge() { + @Test + public void testRuleChainMetadataWithMissingNodeForOldEdgeVersions() { + // GIVEN + RuleChainMetaData metaData = createMetadataWithProblemNodes(CONFIG_TO_MISS_NODE_FOR_OLD_EDGE); + + TEST_SUPPORTED_EDGE_VERSIONS.forEach(edgeVersion -> { + // WHEN + List ruleNodes = getRuleNodesFromUpdateMsg(metaData, edgeVersion); + + // THEN + boolean isOldEdge = EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0); + + if (isOldEdge) { + Assert.assertTrue("Rule Node must be empty", ruleNodes.isEmpty()); + } else { + Assert.assertEquals(MISSING_NODES_IN_VERSION_37.size(), ruleNodes.size()); + } + }); + } + + private RuleChainMetaData createMetadataWithProblemNodes(Map nodeMap) { RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + List ruleNodes = new ArrayList<>(); + + nodeMap.entrySet().forEach(configToNodeName -> { + RuleNode ruleNode = new RuleNode(); - RuleNode ruleNode1 = new RuleNode(); - ruleNode1.setName("TbMsgTimeseriesNode"); - ruleNode1.setType(org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode.class.getName()); - ruleNode1.setConfigurationVersion(CONFIGURATION_VERSION); - ruleNode1.setConfiguration(JacksonUtil.valueToTree(new TbMsgTimeseriesNodeConfiguration().defaultConfiguration())); + ruleNode.setName(configToNodeName.getValue()); + ruleNode.setType(configToNodeName.getValue()); + ruleNode.setConfigurationVersion(CONFIGURATION_VERSION); + ruleNode.setConfiguration(JacksonUtil.valueToTree(configToNodeName.getKey().defaultConfiguration())); + + ruleNodes.add(ruleNode); + }); ruleChainMetaData.setFirstNodeIndex(0); - ruleChainMetaData.setNodes(Collections.singletonList(ruleNode1)); + ruleChainMetaData.setNodes(ruleNodes); return ruleChainMetaData; } - - private RuleNode getRuleNodeFromMetadataUpdateMessage(RuleChainMetaData metaData, EdgeVersion edgeVersion) { + private List getRuleNodesFromUpdateMsg(RuleChainMetaData metaData, EdgeVersion edgeVersion) { RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, metaData, edgeVersion); RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true); Assert.assertNotNull("RuleChainMetaData is null", ruleChainMetaData); - RuleNode ruleNode = ruleChainMetaData.getNodes().stream().findFirst().orElse(null); - Assert.assertNotNull("RuleNode is null for Edge version " + edgeVersion, ruleNode); - Assert.assertNotNull("Configuration is null for Edge version " + edgeVersion, ruleNode.getConfiguration()); + return ruleChainMetaData.getNodes(); + } - return ruleNode; + private void validateRuleNodeConfig(List ruleNodes, EdgeVersion edgeVersion) { + ruleNodes.forEach(ruleNode -> { + int ruleNodeConfigAmount = NODE_TO_CONFIG_PARAMS_COUNT.get(ruleNode.getName()); + + boolean isOldEdge = EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0); + int expectedConfigAmount = isOldEdge ? ruleNodeConfigAmount - 1 : ruleNodeConfigAmount; + boolean includeConfigParam = !isOldEdge; + + validateParams(ruleNode, expectedConfigAmount, includeConfigParam); + }); } - private void assertRuleNodeConfiguration(RuleNode ruleNode, EdgeVersion edgeVersion) { - if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0)) { - Assert.assertEquals("Unexpected config size", 2, ruleNode.getConfiguration().size()); - Assert.assertFalse("Unexpected field 'processingSettings'", ruleNode.getConfiguration().has("processingSettings")); - }else{ - Assert.assertEquals("Unexpected config size", 3, ruleNode.getConfiguration().size()); - Assert.assertTrue("Missing field 'processingSettings'", ruleNode.getConfiguration().has("processingSettings")); - } + private void validateParams(RuleNode ruleNode, int expectedConfigAmount, boolean includeConfigParam) { + String ignoreConfigParam = NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION.get(ruleNode.getName()); + + Assert.assertEquals( + String.format("Expected %d config params for ruleNode '%s', but found %d", expectedConfigAmount, ruleNode.getName(), ruleNode.getConfiguration().size()), + expectedConfigAmount, ruleNode.getConfiguration().size() + ); + + boolean hasIgnoredField = ruleNode.getConfiguration().has(ignoreConfigParam); + Assert.assertEquals( + String.format("Field '%s' for ruleNode '%s' should %s be present", ignoreConfigParam, ruleNode.getName(), includeConfigParam ? "not" : ""), + includeConfigParam, hasIgnoredField + ); } + } From 487d3849b4c07cc1f9e2eb9f2869ae9464b39a6e Mon Sep 17 00:00:00 2001 From: yevhenii Date: Tue, 18 Mar 2025 19:27:57 +0200 Subject: [PATCH 06/54] Fix RuleChainMetadata for older Edge versions - refactoring --- .../service/edge/EdgeMsgConstructorUtils.java | 14 +++--- .../edge/rpc/utils/EdgeVersionUtils.java | 9 ++++ .../edge/EdgeMsgConstructorUtilsTest.java | 45 +++++++++---------- 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 61875f1277..85471f6211 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -129,7 +129,7 @@ import java.util.UUID; @Slf4j public class EdgeMsgConstructorUtils { - public static final Map NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION = Map.of( + public static final Map NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION = Map.of( TbMsgTimeseriesNode.class.getName(), "processingSettings", TbMsgAttributesNode.class.getName(), "processingSettings", TbSaveToCustomCassandraTableNode.class.getName(), "defaultTtl" @@ -452,7 +452,7 @@ public class EdgeMsgConstructorUtils { JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); JsonNode nodes = jsonNode.get("nodes"); - if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0)) { + if (EdgeVersionUtils.isEdgeOlderThan_3_8_0(edgeVersion)) { Iterator iterator = nodes.iterator(); while (iterator.hasNext()) { JsonNode node = iterator.next(); @@ -464,7 +464,7 @@ public class EdgeMsgConstructorUtils { } } - if (EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0)) { + if (EdgeVersionUtils.isEdgeOlderThan_3_9_0(edgeVersion)) { nodes.forEach(EdgeMsgConstructorUtils::changeRuleNodeConfigForOldEdgeVersion); return JacksonUtil.toString(jsonNode); @@ -475,10 +475,10 @@ public class EdgeMsgConstructorUtils { private static void changeRuleNodeConfigForOldEdgeVersion(JsonNode node) { if (node.isObject()) { - JsonNode configurationNode = node.get("configuration"); - if (configurationNode != null && configurationNode.isObject() && - NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION.containsKey(node.get("type").asText())) { - ((ObjectNode) configurationNode).remove(NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION.get(node.get("type").asText())); + String nodeType = node.get("type").asText(); + + if (node.isObject() && node.has("configuration") && NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION.containsKey(nodeType)) { + ((ObjectNode) node.get("configuration")).remove(NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION.get(nodeType)); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java index 07e2024e1f..2ae6e91f34 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java @@ -24,4 +24,13 @@ public final class EdgeVersionUtils { public static boolean isEdgeVersionOlderThan(EdgeVersion currentVersion, EdgeVersion requiredVersion) { return currentVersion.ordinal() < requiredVersion.ordinal(); } + + public static boolean isEdgeOlderThan_3_9_0(EdgeVersion currentVersion) { + return isEdgeVersionOlderThan(currentVersion, EdgeVersion.V_3_9_0); + } + + public static boolean isEdgeOlderThan_3_8_0(EdgeVersion currentVersion) { + return isEdgeVersionOlderThan(currentVersion, EdgeVersion.V_3_8_0); + } + } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index 84ccfdb3ee..937a6709c5 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -33,7 +33,6 @@ import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.gen.edge.v1.EdgeVersion; -import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; @@ -43,13 +42,13 @@ import java.util.List; import java.util.Map; import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.MISSING_NODES_IN_VERSION_37; -import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION; +import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION; @Slf4j public class EdgeMsgConstructorUtilsTest { private static final int CONFIGURATION_VERSION = 5; - public static final List TEST_SUPPORTED_EDGE_VERSIONS = Arrays.asList( + public static final List SUPPORTED_EDGE_VERSIONS_FOR_TESTS = Arrays.asList( EdgeVersion.V_4_0_0, EdgeVersion.V_3_9_0, EdgeVersion.V_3_8_0, EdgeVersion.V_3_7_0 ); @@ -75,12 +74,12 @@ public class EdgeMsgConstructorUtilsTest { // GIVEN RuleChainMetaData metaData = createMetadataWithProblemNodes(CONFIG_TO_NODE_NAME); - TEST_SUPPORTED_EDGE_VERSIONS.forEach(edgeVersion -> { + SUPPORTED_EDGE_VERSIONS_FOR_TESTS.forEach(edgeVersion -> { // WHEN - List ruleNodes = getRuleNodesFromUpdateMsg(metaData, edgeVersion); + List ruleNodes = extractRuleNodesFromUpdateMsg(metaData, edgeVersion); // THEN - validateRuleNodeConfig(ruleNodes, edgeVersion); + assertRuleNodeConfig(ruleNodes, edgeVersion); }); } @@ -89,12 +88,12 @@ public class EdgeMsgConstructorUtilsTest { // GIVEN RuleChainMetaData metaData = createMetadataWithProblemNodes(CONFIG_TO_MISS_NODE_FOR_OLD_EDGE); - TEST_SUPPORTED_EDGE_VERSIONS.forEach(edgeVersion -> { + SUPPORTED_EDGE_VERSIONS_FOR_TESTS.forEach(edgeVersion -> { // WHEN - List ruleNodes = getRuleNodesFromUpdateMsg(metaData, edgeVersion); + List ruleNodes = extractRuleNodesFromUpdateMsg(metaData, edgeVersion); // THEN - boolean isOldEdge = EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_8_0); + boolean isOldEdge = EdgeVersionUtils.isEdgeOlderThan_3_8_0(edgeVersion); if (isOldEdge) { Assert.assertTrue("Rule Node must be empty", ruleNodes.isEmpty()); @@ -108,13 +107,13 @@ public class EdgeMsgConstructorUtilsTest { RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); List ruleNodes = new ArrayList<>(); - nodeMap.entrySet().forEach(configToNodeName -> { + nodeMap.forEach((key, value) -> { RuleNode ruleNode = new RuleNode(); - ruleNode.setName(configToNodeName.getValue()); - ruleNode.setType(configToNodeName.getValue()); + ruleNode.setName(value); + ruleNode.setType(value); ruleNode.setConfigurationVersion(CONFIGURATION_VERSION); - ruleNode.setConfiguration(JacksonUtil.valueToTree(configToNodeName.getKey().defaultConfiguration())); + ruleNode.setConfiguration(JacksonUtil.valueToTree(key.defaultConfiguration())); ruleNodes.add(ruleNode); }); @@ -125,30 +124,30 @@ public class EdgeMsgConstructorUtilsTest { return ruleChainMetaData; } - private List getRuleNodesFromUpdateMsg(RuleChainMetaData metaData, EdgeVersion edgeVersion) { - RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = - EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, metaData, edgeVersion); + private List extractRuleNodesFromUpdateMsg(RuleChainMetaData metaData, EdgeVersion edgeVersion) { + String ruleChainMetadataUpdateMsg = + EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, metaData, edgeVersion).getEntity(); - RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true); + RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg, RuleChainMetaData.class, true); Assert.assertNotNull("RuleChainMetaData is null", ruleChainMetaData); return ruleChainMetaData.getNodes(); } - private void validateRuleNodeConfig(List ruleNodes, EdgeVersion edgeVersion) { + private void assertRuleNodeConfig(List ruleNodes, EdgeVersion edgeVersion) { ruleNodes.forEach(ruleNode -> { - int ruleNodeConfigAmount = NODE_TO_CONFIG_PARAMS_COUNT.get(ruleNode.getName()); + int configParamCount = NODE_TO_CONFIG_PARAMS_COUNT.get(ruleNode.getName()); - boolean isOldEdge = EdgeVersionUtils.isEdgeVersionOlderThan(edgeVersion, EdgeVersion.V_3_9_0); - int expectedConfigAmount = isOldEdge ? ruleNodeConfigAmount - 1 : ruleNodeConfigAmount; - boolean includeConfigParam = !isOldEdge; + boolean isLegacyEdgeVersion = EdgeVersionUtils.isEdgeOlderThan_3_9_0(edgeVersion); + int expectedConfigAmount = isLegacyEdgeVersion ? configParamCount - 1 : configParamCount; + boolean includeConfigParam = !isLegacyEdgeVersion; validateParams(ruleNode, expectedConfigAmount, includeConfigParam); }); } private void validateParams(RuleNode ruleNode, int expectedConfigAmount, boolean includeConfigParam) { - String ignoreConfigParam = NODE_TO_IGNORE_PARAM_FOR_OLD_EDGE_VERSION.get(ruleNode.getName()); + String ignoreConfigParam = NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION.get(ruleNode.getName()); Assert.assertEquals( String.format("Expected %d config params for ruleNode '%s', but found %d", expectedConfigAmount, ruleNode.getName(), ruleNode.getConfiguration().size()), From d602333d98a7347410a9077adc5e876b358893a1 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Wed, 19 Mar 2025 19:30:01 +0200 Subject: [PATCH 07/54] Fix RuleChainMetadata for older Edge versions - refactoring --- .../service/edge/EdgeMsgConstructorUtils.java | 74 +++++++++++-------- .../edge/rpc/utils/EdgeVersionUtils.java | 8 -- .../edge/EdgeMsgConstructorUtilsTest.java | 45 ++++------- 3 files changed, 60 insertions(+), 67 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 85471f6211..d9c9bee9df 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -119,7 +119,6 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.Iterator; import java.util.List; @@ -128,17 +127,30 @@ import java.util.Set; import java.util.UUID; @Slf4j + public class EdgeMsgConstructorUtils { - public static final Map NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION = Map.of( - TbMsgTimeseriesNode.class.getName(), "processingSettings", - TbMsgAttributesNode.class.getName(), "processingSettings", - TbSaveToCustomCassandraTableNode.class.getName(), "defaultTtl" + public static final Map> VERSION_TO_IGNORED_PARAM = Map.of( + EdgeVersion.V_3_8_0, + Map.of( + TbMsgTimeseriesNode.class.getName(), "processingSettings", + TbMsgAttributesNode.class.getName(), "processingSettings", + TbSaveToCustomCassandraTableNode.class.getName(), "defaultTtl" + ), + EdgeVersion.V_3_7_0, + Map.of( + TbMsgTimeseriesNode.class.getName(), "processingSettings", + TbMsgAttributesNode.class.getName(), "processingSettings", + TbSaveToCustomCassandraTableNode.class.getName(), "defaultTtl" + ) ); - //added in edge version 3.8.0 - public static final Set MISSING_NODES_IN_VERSION_37 = Set.of( - TbSendRestApiCallReplyNode.class.getName(), - TbAwsLambdaNode.class.getName() + //these nodes added in edge version 3.8.0 + public static final Map> VERSION_TO_MISSING_NODES = Map.of( + EdgeVersion.V_3_7_0, + Set.of( + TbSendRestApiCallReplyNode.class.getName(), + TbAwsLambdaNode.class.getName() + ) ); public static AlarmUpdateMsg constructAlarmUpdatedMsg(UpdateMsgType msgType, Alarm alarm) { @@ -452,33 +464,35 @@ public class EdgeMsgConstructorUtils { JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); JsonNode nodes = jsonNode.get("nodes"); - if (EdgeVersionUtils.isEdgeOlderThan_3_8_0(edgeVersion)) { - Iterator iterator = nodes.iterator(); - while (iterator.hasNext()) { - JsonNode node = iterator.next(); + changeConfigForOldEdgeVersions(nodes, edgeVersion); + removeMissingNodeOldForEdge(nodes, edgeVersion); - String type = node.get("type").asText(); - if (MISSING_NODES_IN_VERSION_37.contains(type)) { - iterator.remove(); - } - } - } + return JacksonUtil.toString(jsonNode); + } - if (EdgeVersionUtils.isEdgeOlderThan_3_9_0(edgeVersion)) { - nodes.forEach(EdgeMsgConstructorUtils::changeRuleNodeConfigForOldEdgeVersion); + private static void changeConfigForOldEdgeVersions(JsonNode nodes, EdgeVersion edgeVersion) { + nodes.forEach(node -> { + if (node.isObject() && node.has("configuration")) { + String nodeType = node.get("type").asText(); + Map ignoredParams = VERSION_TO_IGNORED_PARAM.get(edgeVersion); - return JacksonUtil.toString(jsonNode); - } else { - return JacksonUtil.toString(ruleChainMetaData); - } + if (ignoredParams != null && ignoredParams.containsKey(nodeType)) { + ((ObjectNode) node.get("configuration")).remove(ignoredParams.get(nodeType)); + } + } + }); } - private static void changeRuleNodeConfigForOldEdgeVersion(JsonNode node) { - if (node.isObject()) { - String nodeType = node.get("type").asText(); + private static void removeMissingNodeOldForEdge(JsonNode nodes, EdgeVersion edgeVersion) { + Iterator iterator = nodes.iterator(); + + while (iterator.hasNext()) { + JsonNode node = iterator.next(); + String type = node.get("type").asText(); + Set missNodes = VERSION_TO_MISSING_NODES.get(edgeVersion); - if (node.isObject() && node.has("configuration") && NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION.containsKey(nodeType)) { - ((ObjectNode) node.get("configuration")).remove(NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION.get(nodeType)); + if (missNodes != null && missNodes.contains(type)) { + iterator.remove(); } } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java index 2ae6e91f34..9902118eb2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/utils/EdgeVersionUtils.java @@ -25,12 +25,4 @@ public final class EdgeVersionUtils { return currentVersion.ordinal() < requiredVersion.ordinal(); } - public static boolean isEdgeOlderThan_3_9_0(EdgeVersion currentVersion) { - return isEdgeVersionOlderThan(currentVersion, EdgeVersion.V_3_9_0); - } - - public static boolean isEdgeOlderThan_3_8_0(EdgeVersion currentVersion) { - return isEdgeVersionOlderThan(currentVersion, EdgeVersion.V_3_8_0); - } - } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index 937a6709c5..690deac0ec 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -34,15 +34,14 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import org.thingsboard.server.service.edge.rpc.utils.EdgeVersionUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; -import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.MISSING_NODES_IN_VERSION_37; -import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION; +import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.VERSION_TO_IGNORED_PARAM; +import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.VERSION_TO_MISSING_NODES; @Slf4j public class EdgeMsgConstructorUtilsTest { @@ -93,13 +92,11 @@ public class EdgeMsgConstructorUtilsTest { List ruleNodes = extractRuleNodesFromUpdateMsg(metaData, edgeVersion); // THEN - boolean isOldEdge = EdgeVersionUtils.isEdgeOlderThan_3_8_0(edgeVersion); + int leftNode = VERSION_TO_MISSING_NODES.containsKey(edgeVersion) ? + CONFIG_TO_MISS_NODE_FOR_OLD_EDGE.size() - VERSION_TO_MISSING_NODES.get(edgeVersion).size() : + CONFIG_TO_MISS_NODE_FOR_OLD_EDGE.size(); - if (isOldEdge) { - Assert.assertTrue("Rule Node must be empty", ruleNodes.isEmpty()); - } else { - Assert.assertEquals(MISSING_NODES_IN_VERSION_37.size(), ruleNodes.size()); - } + Assert.assertEquals(leftNode, ruleNodes.size()); }); } @@ -129,6 +126,7 @@ public class EdgeMsgConstructorUtilsTest { EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, metaData, edgeVersion).getEntity(); RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg, RuleChainMetaData.class, true); + Assert.assertNotNull("RuleChainMetaData is null", ruleChainMetaData); return ruleChainMetaData.getNodes(); @@ -136,29 +134,18 @@ public class EdgeMsgConstructorUtilsTest { private void assertRuleNodeConfig(List ruleNodes, EdgeVersion edgeVersion) { ruleNodes.forEach(ruleNode -> { - int configParamCount = NODE_TO_CONFIG_PARAMS_COUNT.get(ruleNode.getName()); + int configParamCount = NODE_TO_CONFIG_PARAMS_COUNT.get(ruleNode.getType()); - boolean isLegacyEdgeVersion = EdgeVersionUtils.isEdgeOlderThan_3_9_0(edgeVersion); - int expectedConfigAmount = isLegacyEdgeVersion ? configParamCount - 1 : configParamCount; - boolean includeConfigParam = !isLegacyEdgeVersion; + boolean isOldEdgeVersion = VERSION_TO_IGNORED_PARAM.keySet().stream() + .anyMatch(version -> version.equals(edgeVersion)); + int expectedConfigAmount = isOldEdgeVersion ? configParamCount - 1 : configParamCount; - validateParams(ruleNode, expectedConfigAmount, includeConfigParam); + Assert.assertEquals( + String.format("Expected %d config params for ruleNode '%s', but found %d", + expectedConfigAmount, ruleNode.getName(), ruleNode.getConfiguration().size()), + expectedConfigAmount, ruleNode.getConfiguration().size() + ); }); } - private void validateParams(RuleNode ruleNode, int expectedConfigAmount, boolean includeConfigParam) { - String ignoreConfigParam = NODE_TO_IGNORED_PARAM_FOR_OLD_EDGE_VERSION.get(ruleNode.getName()); - - Assert.assertEquals( - String.format("Expected %d config params for ruleNode '%s', but found %d", expectedConfigAmount, ruleNode.getName(), ruleNode.getConfiguration().size()), - expectedConfigAmount, ruleNode.getConfiguration().size() - ); - - boolean hasIgnoredField = ruleNode.getConfiguration().has(ignoreConfigParam); - Assert.assertEquals( - String.format("Field '%s' for ruleNode '%s' should %s be present", ignoreConfigParam, ruleNode.getName(), includeConfigParam ? "not" : ""), - includeConfigParam, hasIgnoredField - ); - } - } From 65f5cd30936cc01fbd022e1a1ae8a65a50193fad Mon Sep 17 00:00:00 2001 From: yevhenii Date: Thu, 20 Mar 2025 11:43:34 +0200 Subject: [PATCH 08/54] Fix RuleChainMetadata for older Edge versions - refactoring --- .../server/service/edge/EdgeMsgConstructorUtils.java | 5 ++--- .../server/service/edge/EdgeMsgConstructorUtilsTest.java | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index d9c9bee9df..fb6d8d7f92 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -144,7 +144,6 @@ public class EdgeMsgConstructorUtils { ) ); - //these nodes added in edge version 3.8.0 public static final Map> VERSION_TO_MISSING_NODES = Map.of( EdgeVersion.V_3_7_0, Set.of( @@ -465,7 +464,7 @@ public class EdgeMsgConstructorUtils { JsonNode nodes = jsonNode.get("nodes"); changeConfigForOldEdgeVersions(nodes, edgeVersion); - removeMissingNodeOldForEdge(nodes, edgeVersion); + removeMissingNodeForOldEdge(nodes, edgeVersion); return JacksonUtil.toString(jsonNode); } @@ -483,7 +482,7 @@ public class EdgeMsgConstructorUtils { }); } - private static void removeMissingNodeOldForEdge(JsonNode nodes, EdgeVersion edgeVersion) { + private static void removeMissingNodeForOldEdge(JsonNode nodes, EdgeVersion edgeVersion) { Iterator iterator = nodes.iterator(); while (iterator.hasNext()) { diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index 690deac0ec..702eed26b5 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -136,13 +136,13 @@ public class EdgeMsgConstructorUtilsTest { ruleNodes.forEach(ruleNode -> { int configParamCount = NODE_TO_CONFIG_PARAMS_COUNT.get(ruleNode.getType()); - boolean isOldEdgeVersion = VERSION_TO_IGNORED_PARAM.keySet().stream() - .anyMatch(version -> version.equals(edgeVersion)); + boolean isOldEdgeVersion = VERSION_TO_IGNORED_PARAM.entrySet().stream() + .anyMatch(entry -> entry.getKey().equals(edgeVersion) && + entry.getValue().containsKey(ruleNode.getType())); int expectedConfigAmount = isOldEdgeVersion ? configParamCount - 1 : configParamCount; Assert.assertEquals( - String.format("Expected %d config params for ruleNode '%s', but found %d", - expectedConfigAmount, ruleNode.getName(), ruleNode.getConfiguration().size()), + String.format("For ruleNode '%s', edgeVersion '%s", ruleNode.getName(), edgeVersion), expectedConfigAmount, ruleNode.getConfiguration().size() ); }); From 1675e3aa50610b2df7578d7dd209169cf419017d Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Fri, 21 Mar 2025 11:20:47 +0200 Subject: [PATCH 09/54] Fix RuleChainMetadata for older Edge versions - renaming --- .../service/edge/EdgeMsgConstructorUtils.java | 20 +++++++++---------- .../edge/EdgeMsgConstructorUtilsTest.java | 10 +++++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index fb6d8d7f92..309c24d699 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -129,7 +129,7 @@ import java.util.UUID; @Slf4j public class EdgeMsgConstructorUtils { - public static final Map> VERSION_TO_IGNORED_PARAM = Map.of( + public static final Map> IGNORED_PARAMS_BY_EDGE_VERSION = Map.of( EdgeVersion.V_3_8_0, Map.of( TbMsgTimeseriesNode.class.getName(), "processingSettings", @@ -144,7 +144,7 @@ public class EdgeMsgConstructorUtils { ) ); - public static final Map> VERSION_TO_MISSING_NODES = Map.of( + public static final Map> EXCLUDED_NODES_BY_EDGE_VERSION = Map.of( EdgeVersion.V_3_7_0, Set.of( TbSendRestApiCallReplyNode.class.getName(), @@ -451,7 +451,7 @@ public class EdgeMsgConstructorUtils { } public static RuleChainMetadataUpdateMsg constructRuleChainMetadataUpdatedMsg(UpdateMsgType msgType, RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { - String metaData = filterMetadataForOldEdgeVersions(ruleChainMetaData, edgeVersion); + String metaData = sanitizeMetadataForLegacyEdgeVersion(ruleChainMetaData, edgeVersion); return RuleChainMetadataUpdateMsg.newBuilder() .setMsgType(msgType) @@ -459,21 +459,21 @@ public class EdgeMsgConstructorUtils { .build(); } - private static String filterMetadataForOldEdgeVersions(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { + private static String sanitizeMetadataForLegacyEdgeVersion(RuleChainMetaData ruleChainMetaData, EdgeVersion edgeVersion) { JsonNode jsonNode = JacksonUtil.valueToTree(ruleChainMetaData); JsonNode nodes = jsonNode.get("nodes"); - changeConfigForOldEdgeVersions(nodes, edgeVersion); - removeMissingNodeForOldEdge(nodes, edgeVersion); + updateNodeConfigurationsForLegacyEdge(nodes, edgeVersion); + removeExcludedNodesForLegacyEdge(nodes, edgeVersion); return JacksonUtil.toString(jsonNode); } - private static void changeConfigForOldEdgeVersions(JsonNode nodes, EdgeVersion edgeVersion) { + private static void updateNodeConfigurationsForLegacyEdge(JsonNode nodes, EdgeVersion edgeVersion) { nodes.forEach(node -> { if (node.isObject() && node.has("configuration")) { String nodeType = node.get("type").asText(); - Map ignoredParams = VERSION_TO_IGNORED_PARAM.get(edgeVersion); + Map ignoredParams = IGNORED_PARAMS_BY_EDGE_VERSION.get(edgeVersion); if (ignoredParams != null && ignoredParams.containsKey(nodeType)) { ((ObjectNode) node.get("configuration")).remove(ignoredParams.get(nodeType)); @@ -482,13 +482,13 @@ public class EdgeMsgConstructorUtils { }); } - private static void removeMissingNodeForOldEdge(JsonNode nodes, EdgeVersion edgeVersion) { + private static void removeExcludedNodesForLegacyEdge(JsonNode nodes, EdgeVersion edgeVersion) { Iterator iterator = nodes.iterator(); while (iterator.hasNext()) { JsonNode node = iterator.next(); String type = node.get("type").asText(); - Set missNodes = VERSION_TO_MISSING_NODES.get(edgeVersion); + Set missNodes = EXCLUDED_NODES_BY_EDGE_VERSION.get(edgeVersion); if (missNodes != null && missNodes.contains(type)) { iterator.remove(); diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index 702eed26b5..ea1f99d41a 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -40,8 +40,8 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.VERSION_TO_IGNORED_PARAM; -import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.VERSION_TO_MISSING_NODES; +import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.EXCLUDED_NODES_BY_EDGE_VERSION; +import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.IGNORED_PARAMS_BY_EDGE_VERSION; @Slf4j public class EdgeMsgConstructorUtilsTest { @@ -92,8 +92,8 @@ public class EdgeMsgConstructorUtilsTest { List ruleNodes = extractRuleNodesFromUpdateMsg(metaData, edgeVersion); // THEN - int leftNode = VERSION_TO_MISSING_NODES.containsKey(edgeVersion) ? - CONFIG_TO_MISS_NODE_FOR_OLD_EDGE.size() - VERSION_TO_MISSING_NODES.get(edgeVersion).size() : + int leftNode = EXCLUDED_NODES_BY_EDGE_VERSION.containsKey(edgeVersion) ? + CONFIG_TO_MISS_NODE_FOR_OLD_EDGE.size() - EXCLUDED_NODES_BY_EDGE_VERSION.get(edgeVersion).size() : CONFIG_TO_MISS_NODE_FOR_OLD_EDGE.size(); Assert.assertEquals(leftNode, ruleNodes.size()); @@ -136,7 +136,7 @@ public class EdgeMsgConstructorUtilsTest { ruleNodes.forEach(ruleNode -> { int configParamCount = NODE_TO_CONFIG_PARAMS_COUNT.get(ruleNode.getType()); - boolean isOldEdgeVersion = VERSION_TO_IGNORED_PARAM.entrySet().stream() + boolean isOldEdgeVersion = IGNORED_PARAMS_BY_EDGE_VERSION.entrySet().stream() .anyMatch(entry -> entry.getKey().equals(edgeVersion) && entry.getValue().containsKey(ruleNode.getType())); int expectedConfigAmount = isOldEdgeVersion ? configParamCount - 1 : configParamCount; From 6f11005095641fc7faaabefe54b09d0926f45623 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Fri, 21 Mar 2025 11:30:55 +0200 Subject: [PATCH 10/54] Fix RuleChainMetadata for older Edge versions - remove space --- .../thingsboard/server/service/edge/EdgeMsgConstructorUtils.java | 1 - 1 file changed, 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 309c24d699..9bcd8fc373 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -127,7 +127,6 @@ import java.util.Set; import java.util.UUID; @Slf4j - public class EdgeMsgConstructorUtils { public static final Map> IGNORED_PARAMS_BY_EDGE_VERSION = Map.of( EdgeVersion.V_3_8_0, From 8f1fff61dfd55fc5e757f61e968f14f79da15363 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Fri, 21 Mar 2025 16:31:30 +0200 Subject: [PATCH 11/54] Fix equals method for CF --- .../cf/DefaultCalculatedFieldCache.java | 3 + .../DefaultEntitiesExportImportService.java | 2 - .../service/sync/vc/VersionControlTest.java | 2 + .../common/data/cf/CalculatedField.java | 57 ++++++++++++++----- .../ScriptCalculatedFieldConfiguration.java | 3 + .../SimpleCalculatedFieldConfiguration.java | 2 + 6 files changed, 53 insertions(+), 16 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java index 64487d9b3e..219a261183 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java @@ -144,6 +144,9 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { calculatedFieldFetchLock.lock(); try { CalculatedField calculatedField = calculatedFieldService.findById(tenantId, calculatedFieldId); + if (calculatedField == null) { + return; + } EntityId cfEntityId = calculatedField.getEntityId(); calculatedFields.put(calculatedFieldId, calculatedField); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index 06fe7f4036..db7e37b368 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityImportResult; import org.thingsboard.server.common.data.util.ThrowingRunnable; -import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.queue.util.TbCoreComponent; @@ -62,7 +61,6 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS private final Map> importServices = new HashMap<>(); private final RelationService relationService; - private final CalculatedFieldService calculatedFieldService; private final RateLimitService rateLimitService; private final TbLogEntityActionService logEntityActionService; 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 06f61ca4e5..29c3af5387 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 @@ -621,6 +621,7 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedField.getName()).isEqualTo(deviceCalculatedField.getName()); assertThat(importedField.getType()).isEqualTo(deviceCalculatedField.getType()); assertThat(importedField.getId()).isNotEqualTo(deviceCalculatedField.getId()); + assertThat(importedField.getConfiguration().getArguments().get("T").getRefEntityId()).isEqualTo(importedAsset.getId()); }); List importedAssetCalculatedFields = findCalculatedFieldsByEntityId(importedAsset.getId()); @@ -629,6 +630,7 @@ public class VersionControlTest extends AbstractControllerTest { assertThat(importedField.getName()).isEqualTo(assetCalculatedField.getName()); assertThat(importedField.getType()).isEqualTo(assetCalculatedField.getType()); assertThat(importedField.getId()).isNotEqualTo(assetCalculatedField.getId()); + assertThat(importedField.getConfiguration().getArguments().get("T").getRefEntityId()).isEqualTo(importedDevice.getId()); }); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java index b86f30ca78..ea7f81f216 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedField.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSetter; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import org.thingsboard.server.common.data.BaseData; @@ -37,10 +36,10 @@ import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serial; +import java.util.Objects; @Schema @Data -@EqualsAndHashCode(callSuper = true) public class CalculatedField extends BaseData implements HasName, HasTenantId, HasVersion, HasDebugSettings { @Serial @@ -112,6 +111,48 @@ public class CalculatedField extends BaseData implements HasN return super.getCreatedTime(); } + // Getter is ignored for serialization + @JsonIgnore + public boolean isDebugMode() { + return debugMode; + } + + // Setter is annotated for deserialization + @JsonSetter + public void setDebugMode(boolean debugMode) { + this.debugMode = debugMode; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof CalculatedField that)) return false; + if (!super.equals(o)) return false; + return Objects.equals(tenantId, that.tenantId) && + Objects.equals(entityId, that.entityId) && + Objects.equals(name, that.name) && + Objects.equals(debugSettings, that.debugSettings) && + Objects.equals(configuration, that.configuration) && + type == that.type && debugMode == that.debugMode && + configurationVersion == that.configurationVersion && + Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Objects.hashCode(tenantId); + result = 31 * result + Objects.hashCode(entityId); + result = 31 * result + Objects.hashCode(type); + result = 31 * result + Objects.hashCode(name); + result = 31 * result + Boolean.hashCode(debugMode); + result = 31 * result + Objects.hashCode(debugSettings); + result = 31 * result + Integer.hashCode(configurationVersion); + result = 31 * result + Objects.hashCode(configuration); + result = 31 * result + Objects.hashCode(version); + return result; + } + @Override public String toString() { return new StringBuilder() @@ -128,16 +169,4 @@ public class CalculatedField extends BaseData implements HasN .toString(); } - // Getter is ignored for serialization - @JsonIgnore - public boolean isDebugMode() { - return debugMode; - } - - // Setter is annotated for deserialization - @JsonSetter - public void setDebugMode(boolean debugMode) { - this.debugMode = debugMode; - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java index 0971217fdf..c2dde43b8e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScriptCalculatedFieldConfiguration.java @@ -16,13 +16,16 @@ package org.thingsboard.server.common.data.cf.configuration; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @Data +@EqualsAndHashCode(callSuper = true) public class ScriptCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { @Override public CalculatedFieldType getType() { return CalculatedFieldType.SCRIPT; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java index 79a0518ba0..0a422de175 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java @@ -16,9 +16,11 @@ package org.thingsboard.server.common.data.cf.configuration; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @Data +@EqualsAndHashCode(callSuper = true) public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { @Override From 77721d5684968e9e72c487a72659423106e1e57d Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 24 Mar 2025 09:49:40 +0200 Subject: [PATCH 12/54] Fix CFControllerTest --- .../server/controller/CalculatedFieldControllerTest.java | 9 ++++----- .../SimpleCalculatedFieldConfiguration.java | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java index ee66f664cc..af43b34558 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java @@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -85,7 +84,7 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId()); assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); - assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig(testDevice.getId())); + assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig()); assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); savedCalculatedField.setName("Test CF"); @@ -134,16 +133,16 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { calculatedField.setType(CalculatedFieldType.SIMPLE); calculatedField.setName("Test Calculated Field"); calculatedField.setConfigurationVersion(1); - calculatedField.setConfiguration(getCalculatedFieldConfig(null)); + calculatedField.setConfiguration(getCalculatedFieldConfig()); calculatedField.setVersion(1L); return calculatedField; } - private CalculatedFieldConfiguration getCalculatedFieldConfig(EntityId referencedEntityId) { + private CalculatedFieldConfiguration getCalculatedFieldConfig() { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); Argument argument = new Argument(); - argument.setRefEntityId(referencedEntityId); + argument.setRefEntityId(null); ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); argument.setRefEntityKey(refEntityKey); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java index 0a422de175..5c0ce71e86 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/SimpleCalculatedFieldConfiguration.java @@ -27,4 +27,5 @@ public class SimpleCalculatedFieldConfiguration extends BaseCalculatedFieldConfi public CalculatedFieldType getType() { return CalculatedFieldType.SIMPLE; } + } From 3f089da378ec5debdf44d3f777222aac56415f5e Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Mon, 24 Mar 2025 11:57:46 +0200 Subject: [PATCH 13/54] Fix tbCalculatedFieldNotificationsTopic --- .../org/thingsboard/server/queue/discovery/TopicService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java index 5992083d85..cfd796e361 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/TopicService.java @@ -47,7 +47,7 @@ public class TopicService { @Value("${queue.edge.event-notifications-topic:tb_edge_event.notifications}") private String tbEdgeEventNotificationsTopic; - @Value("${queue.calculated_fields.notifications-topic:calculated_field.notifications}") + @Value("${queue.calculated-fields.notifications-topic:calculated_field.notifications}") private String tbCalculatedFieldNotificationsTopic; private final ConcurrentMap tbCoreNotificationTopics = new ConcurrentHashMap<>(); From fabf6f32351193343c20a331f344a147ef56ccc1 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 25 Mar 2025 18:28:30 +0200 Subject: [PATCH 14/54] fixed telemetry update handling for timeseries subscription --- .../DefaultTbLocalSubscriptionService.java | 8 ++--- .../service/ws/DefaultWebSocketService.java | 3 ++ .../sub/TelemetrySubscriptionUpdate.java | 1 + .../controller/TbTestWebSocketClient.java | 14 +++++++++ .../server/controller/WebsocketApiTest.java | 29 +++++++++++++++++++ 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index 59681d3daa..165b0824f5 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -343,11 +343,9 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer s -> { TbTimeSeriesSubscription sub = (TbTimeSeriesSubscription) s; List updateData = null; - if (sub.isAllKeys()) { - updateData = data; - } else { - for (TsKvEntry kv : data) { - if (sub.getKeyStates().containsKey((kv.getKey()))) { + for (TsKvEntry kv : data) { + if (sub.getKeyStates().containsKey((kv.getKey()))) { + if (!sub.isLatestValues() || kv.getTs() > sub.getKeyStates().get(kv.getKey())) { if (updateData == null) { updateData = new ArrayList<>(); } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index 7288a8bac9..c52a20754d 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -123,6 +123,7 @@ public class DefaultWebSocketService implements WebSocketService { private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!"; private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!"; private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!"; + private static final String LATEST_TELEMETRY_SCOPE = "LATEST_TELEMETRY"; private final ConcurrentMap wsSessionsMap = new ConcurrentHashMap<>(); @@ -684,6 +685,7 @@ public class DefaultWebSocketService implements WebSocketService { .queryTs(queryTs) .allKeys(true) .keyStates(subState) + .latestValues(LATEST_TELEMETRY_SCOPE.equals(cmd.getScope())) .build(); subLock.lock(); @@ -739,6 +741,7 @@ public class DefaultWebSocketService implements WebSocketService { .queryTs(queryTs) .allKeys(false) .keyStates(subState) + .latestValues(LATEST_TELEMETRY_SCOPE.equals(cmd.getScope())) .build(); subLock.lock(); diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java index b22b021a03..1a3046e301 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.ws.telemetry.sub; import lombok.AllArgsConstructor; +import net.minidev.json.annotate.JsonIgnore; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.service.subscription.SubscriptionErrorCode; diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index 959db43125..1c5b1dd154 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -31,6 +31,7 @@ import org.thingsboard.server.service.ws.AuthCmd; import org.thingsboard.server.service.ws.WsCmd; import org.thingsboard.server.service.ws.WsCommandsWrapper; import org.thingsboard.server.service.ws.telemetry.cmd.v1.AttributesSubscriptionCmd; +import org.thingsboard.server.service.ws.telemetry.cmd.v1.TimeseriesSubscriptionCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataCmd; @@ -38,6 +39,7 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityHistoryCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.LatestValueCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.TimeSeriesCmd; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.net.URI; import java.nio.channels.NotYetConnectedException; @@ -271,6 +273,18 @@ public class TbTestWebSocketClient extends WebSocketClient { return sendEntityDataQuery(edq); } + public JsonNode sendTimeseriesCmd(EntityId entityId, String scope) { + log.warn("sendTimeseriesCmd entityId: {}, scope: {}", entityId, scope); + TimeseriesSubscriptionCmd cmd = new TimeseriesSubscriptionCmd(0, 0, 0, 10, null); + cmd.setEntityId(entityId.getId().toString()); + cmd.setEntityType(entityId.getEntityType().toString()); + cmd.setCmdId(1); + cmd.setScope(scope); + send(cmd); + String msg = this.waitForReply(); + return JacksonUtil.fromString(msg, JsonNode.class); + } + public void send(WsCmd... cmds) { WsCommandsWrapper cmdsWrapper = new WsCommandsWrapper(); cmdsWrapper.setCmds(List.of(cmds)); diff --git a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java index 9801907d3b..3842cf917b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WebsocketApiTest.java @@ -68,11 +68,13 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmStatusUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityDataUpdate; +import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -584,6 +586,33 @@ public class WebsocketApiTest extends AbstractControllerTest { Assert.assertNull(msg); } + @Test + public void testTimeseriesSubscriptionCmd() throws Exception { + long now = System.currentTimeMillis() - 100; + + long lastTs = now - TimeUnit.MINUTES.toMillis(1); + TsKvEntry dataPoint1 = new BasicTsKvEntry(lastTs, new LongDataEntry("temperature", 42L)); + sendTelemetry(device, List.of(dataPoint1)); + + JsonNode update = getWsClient().sendTimeseriesCmd(device.getId(), "LATEST_TELEMETRY"); + JsonNode data = update.get("data"); + Assert.assertEquals(1, data.size()); + Assert.assertEquals(JacksonUtil.newArrayNode().add(lastTs).add("42"), data.get("temperature").get(0)); + + //Sending update from the past, while latest value has new timestamp; + TsKvEntry dataPoint4 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(5), new LongDataEntry("temperature", 45L)); + getWsClient().registerWaitForUpdate(); + sendTelemetry(device, List.of(dataPoint4)); + String msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + Assert.assertNull(msg); + + //Sending duplicate update again + getWsClient().registerWaitForUpdate(); + sendTelemetry(device, List.of(dataPoint4)); + msg = getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(1)); + Assert.assertNull(msg); + } + @Test public void testEntityDataLatestTsWsCmd() throws Exception { long now = System.currentTimeMillis(); From 3f13fbd23f3df2679fb604ec7fbb391a4f6c511e Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 26 Mar 2025 13:24:49 +0200 Subject: [PATCH 15/54] fixed telemetry update handling for all keys --- .../DefaultTbLocalSubscriptionService.java | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index 165b0824f5..627b7f8b63 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -343,13 +343,28 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer s -> { TbTimeSeriesSubscription sub = (TbTimeSeriesSubscription) s; List updateData = null; - for (TsKvEntry kv : data) { - if (sub.getKeyStates().containsKey((kv.getKey()))) { - if (!sub.isLatestValues() || kv.getTs() > sub.getKeyStates().get(kv.getKey())) { - if (updateData == null) { - updateData = new ArrayList<>(); + if (sub.isAllKeys()) { + if (sub.isLatestValues()) { + for (TsKvEntry kv : data) { + if (!sub.getKeyStates().containsKey((kv.getKey())) || kv.getTs() > sub.getKeyStates().get(kv.getKey())) { + if (updateData == null) { + updateData = new ArrayList<>(); + } + updateData.add(kv); + } + } + } else { + updateData = data; + } + } else { + for (TsKvEntry kv : data) { + if (sub.getKeyStates().containsKey((kv.getKey()))) { + if (!sub.isLatestValues() || kv.getTs() > sub.getKeyStates().get(kv.getKey())) { + if (updateData == null) { + updateData = new ArrayList<>(); + } + updateData.add(kv); } - updateData.add(kv); } } } From 8441a6bca28acc9157b456adf7ee391b445d0121 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 26 Mar 2025 17:29:06 +0200 Subject: [PATCH 16/54] code refactoring --- .../DefaultTbLocalSubscriptionService.java | 7 +- .../service/ws/DefaultWebSocketService.java | 65 ++++++++----------- .../sub/TelemetrySubscriptionUpdate.java | 1 - .../server/common/data/DataConstants.java | 2 + 4 files changed, 32 insertions(+), 43 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index 627b7f8b63..d39c5fcb12 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -343,10 +343,11 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer s -> { TbTimeSeriesSubscription sub = (TbTimeSeriesSubscription) s; List updateData = null; + Map keyStates = sub.getKeyStates(); if (sub.isAllKeys()) { if (sub.isLatestValues()) { for (TsKvEntry kv : data) { - if (!sub.getKeyStates().containsKey((kv.getKey())) || kv.getTs() > sub.getKeyStates().get(kv.getKey())) { + if (!keyStates.containsKey((kv.getKey())) || kv.getTs() > keyStates.get(kv.getKey())) { if (updateData == null) { updateData = new ArrayList<>(); } @@ -358,8 +359,8 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } } else { for (TsKvEntry kv : data) { - if (sub.getKeyStates().containsKey((kv.getKey()))) { - if (!sub.isLatestValues() || kv.getTs() > sub.getKeyStates().get(kv.getKey())) { + if (keyStates.containsKey((kv.getKey()))) { + if (!sub.isLatestValues() || kv.getTs() > keyStates.get(kv.getKey())) { if (updateData == null) { updateData = new ArrayList<>(); } diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index c52a20754d..11d962eb7c 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -105,6 +105,8 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.DataConstants.LATEST_TELEMETRY_SCOPE; + /** * Created by ashvayka on 27.03.18. */ @@ -123,7 +125,6 @@ public class DefaultWebSocketService implements WebSocketService { private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!"; private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!"; private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!"; - private static final String LATEST_TELEMETRY_SCOPE = "LATEST_TELEMETRY"; private final ConcurrentMap wsSessionsMap = new ConcurrentHashMap<>(); @@ -668,25 +669,7 @@ public class DefaultWebSocketService implements WebSocketService { data.forEach(v -> subState.put(v.getKey(), v.getTs())); Lock subLock = new ReentrantLock(); - TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder() - .serviceId(serviceId) - .sessionId(sessionId) - .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId())) - .tenantId(sessionRef.getSecurityCtx().getTenantId()) - .entityId(entityId) - .updateProcessor((subscription, update) -> { - subLock.lock(); - try { - sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update); - } finally { - subLock.unlock(); - } - }) - .queryTs(queryTs) - .allKeys(true) - .keyStates(subState) - .latestValues(LATEST_TELEMETRY_SCOPE.equals(cmd.getScope())) - .build(); + TbTimeSeriesSubscription sub = createTbTimeSeriesSubscription(subState, subLock, sessionId, sessionRef, cmd, entityId, queryTs, true); subLock.lock(); try { @@ -714,6 +697,28 @@ public class DefaultWebSocketService implements WebSocketService { on(r -> Futures.addCallback(tsService.findAllLatest(sessionRef.getSecurityCtx().getTenantId(), entityId), callback, executor), callback::onFailure)); } + private TbTimeSeriesSubscription createTbTimeSeriesSubscription(Map subState, Lock subLock, String sessionId, WebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd, EntityId entityId, long queryTs, boolean allKeys) { + return TbTimeSeriesSubscription.builder() + .serviceId(serviceId) + .sessionId(sessionId) + .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId())) + .tenantId(sessionRef.getSecurityCtx().getTenantId()) + .entityId(entityId) + .updateProcessor((subscription, update) -> { + subLock.lock(); + try { + sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update); + } finally { + subLock.unlock(); + } + }) + .queryTs(queryTs) + .allKeys(allKeys) + .keyStates(subState) + .latestValues(LATEST_TELEMETRY_SCOPE.equals(cmd.getScope())) + .build(); + } + private FutureCallback> getSubscriptionCallback(final WebSocketSessionRef sessionRef, final TimeseriesSubscriptionCmd cmd, final String sessionId, final EntityId entityId, final long queryTs, final long startTs, final List keys) { return new FutureCallback<>() { @@ -724,25 +729,7 @@ public class DefaultWebSocketService implements WebSocketService { data.forEach(v -> subState.put(v.getKey(), v.getTs())); Lock subLock = new ReentrantLock(); - TbTimeSeriesSubscription sub = TbTimeSeriesSubscription.builder() - .serviceId(serviceId) - .sessionId(sessionId) - .subscriptionId(registerNewSessionSubId(sessionId, sessionRef, cmd.getCmdId())) - .tenantId(sessionRef.getSecurityCtx().getTenantId()) - .entityId(entityId) - .updateProcessor((subscription, update) -> { - subLock.lock(); - try { - sendUpdate(subscription.getSessionId(), cmd.getCmdId(), update); - } finally { - subLock.unlock(); - } - }) - .queryTs(queryTs) - .allKeys(false) - .keyStates(subState) - .latestValues(LATEST_TELEMETRY_SCOPE.equals(cmd.getScope())) - .build(); + TbTimeSeriesSubscription sub = createTbTimeSeriesSubscription(subState, subLock, sessionId, sessionRef, cmd, entityId, queryTs, false); subLock.lock(); try { diff --git a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java index 1a3046e301..b22b021a03 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/telemetry/sub/TelemetrySubscriptionUpdate.java @@ -16,7 +16,6 @@ package org.thingsboard.server.service.ws.telemetry.sub; import lombok.AllArgsConstructor; -import net.minidev.json.annotate.JsonIgnore; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.service.subscription.SubscriptionErrorCode; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index b53d6daec2..b2d9d59cca 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -148,4 +148,6 @@ public class DataConstants { public static final String CF_QUEUE_NAME = "CalculatedFields"; public static final String CF_STATES_QUEUE_NAME = "CalculatedFieldStates"; + public static final String LATEST_TELEMETRY_SCOPE = "LATEST_TELEMETRY"; + } From 78d59a99355554ff24ce1409a724f5445689c2d5 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 26 Mar 2025 17:35:36 +0200 Subject: [PATCH 17/54] method renamed --- .../server/service/ws/DefaultWebSocketService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index 11d962eb7c..2e4fe9730a 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -669,7 +669,7 @@ public class DefaultWebSocketService implements WebSocketService { data.forEach(v -> subState.put(v.getKey(), v.getTs())); Lock subLock = new ReentrantLock(); - TbTimeSeriesSubscription sub = createTbTimeSeriesSubscription(subState, subLock, sessionId, sessionRef, cmd, entityId, queryTs, true); + TbTimeSeriesSubscription sub = getTsSubscription(subState, subLock, sessionId, sessionRef, cmd, entityId, queryTs, true); subLock.lock(); try { @@ -697,7 +697,7 @@ public class DefaultWebSocketService implements WebSocketService { on(r -> Futures.addCallback(tsService.findAllLatest(sessionRef.getSecurityCtx().getTenantId(), entityId), callback, executor), callback::onFailure)); } - private TbTimeSeriesSubscription createTbTimeSeriesSubscription(Map subState, Lock subLock, String sessionId, WebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd, EntityId entityId, long queryTs, boolean allKeys) { + private TbTimeSeriesSubscription getTsSubscription(Map subState, Lock subLock, String sessionId, WebSocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd, EntityId entityId, long queryTs, boolean allKeys) { return TbTimeSeriesSubscription.builder() .serviceId(serviceId) .sessionId(sessionId) @@ -729,7 +729,7 @@ public class DefaultWebSocketService implements WebSocketService { data.forEach(v -> subState.put(v.getKey(), v.getTs())); Lock subLock = new ReentrantLock(); - TbTimeSeriesSubscription sub = createTbTimeSeriesSubscription(subState, subLock, sessionId, sessionRef, cmd, entityId, queryTs, false); + TbTimeSeriesSubscription sub = getTsSubscription(subState, subLock, sessionId, sessionRef, cmd, entityId, queryTs, false); subLock.lock(); try { From 0dfef548816632fd54a0daffa4173f5085191ce2 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Thu, 27 Mar 2025 14:41:01 +0100 Subject: [PATCH 18/54] Added ability to use query timeout for JdbcTemplates --- ...medParameterJdbcTemplateConfiguration.java | 43 +++++++++++++++++++ .../server/dao/sql/JdbcTemplateTest.java | 39 +++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/NamedParameterJdbcTemplateConfiguration.java create mode 100644 dao/src/test/java/org/thingsboard/server/dao/sql/JdbcTemplateTest.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/NamedParameterJdbcTemplateConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/sql/NamedParameterJdbcTemplateConfiguration.java new file mode 100644 index 0000000000..ef8d0ed064 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/NamedParameterJdbcTemplateConfiguration.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +@RequiredArgsConstructor +@Slf4j +public class NamedParameterJdbcTemplateConfiguration { + + @Value("${spring.jpa.properties.javax.persistence.query.timeout:30000}") + private int queryTimeout; + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + @PostConstruct + private void init() { + int timeout = Math.max(1, (int) TimeUnit.MILLISECONDS.toSeconds(queryTimeout)); + log.info("Set jdbcTemplate query timeout [{}] second(s)", timeout); + namedParameterJdbcTemplate.getJdbcTemplate().setQueryTimeout(timeout); + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/JdbcTemplateTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/JdbcTemplateTest.java new file mode 100644 index 0000000000..d0af99021a --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/JdbcTemplateTest.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.sql; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.test.context.TestPropertySource; +import org.thingsboard.server.dao.AbstractJpaDaoTest; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +@TestPropertySource(properties = { + "spring.jpa.properties.javax.persistence.query.timeout=500" +}) +public class JdbcTemplateTest extends AbstractJpaDaoTest { + + @Autowired + private NamedParameterJdbcTemplate jdbcTemplate; + + @Test + public void queryTimeoutTest() { + assertThrows(DataAccessResourceFailureException.class, () -> jdbcTemplate.query("SELECT pg_sleep(10)", rs -> {})); + } +} From 27ff3a126cb6e172fda2a28446215d9417ac84a5 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Mon, 31 Mar 2025 13:10:29 +0300 Subject: [PATCH 19/54] Fix RuleChainMetadata for older Edge versions - refactoring test --- .../edge/EdgeMsgConstructorUtilsTest.java | 124 ++++++++++-------- 1 file changed, 66 insertions(+), 58 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index ea1f99d41a..ea82ec1780 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -17,7 +17,11 @@ package org.thingsboard.server.service.edge; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode; import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNodeConfiguration; @@ -32,11 +36,12 @@ import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import java.util.ArrayList; -import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -44,106 +49,109 @@ import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.EXCLUD import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.IGNORED_PARAMS_BY_EDGE_VERSION; @Slf4j +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class EdgeMsgConstructorUtilsTest { private static final int CONFIGURATION_VERSION = 5; - public static final List SUPPORTED_EDGE_VERSIONS_FOR_TESTS = Arrays.asList( - EdgeVersion.V_4_0_0, EdgeVersion.V_3_9_0, EdgeVersion.V_3_8_0, EdgeVersion.V_3_7_0 - ); - - private static final Map CONFIG_TO_NODE_NAME = Map.of( + private static final Map NODE_CONFIG_TO_NAME_MAP = Map.of( new TbMsgTimeseriesNodeConfiguration(), TbMsgTimeseriesNode.class.getName(), new TbMsgAttributesNodeConfiguration(), TbMsgAttributesNode.class.getName(), new TbSaveToCustomCassandraTableNodeConfiguration(), TbSaveToCustomCassandraTableNode.class.getName() ); - private static final Map NODE_TO_CONFIG_PARAMS_COUNT = Map.of( + private static final Map NODE_NAME_TO_CONFIG_PARAM_COUNT_MAP = Map.of( TbMsgTimeseriesNode.class.getName(), 3, TbMsgAttributesNode.class.getName(), 5, TbSaveToCustomCassandraTableNode.class.getName(), 3 ); - private static final Map CONFIG_TO_MISS_NODE_FOR_OLD_EDGE = Map.of( + + private static final Map MISSING_NODE_CONFIGS_FOR_OLD_EDGES = Map.of( new TbSendRestApiCallReplyNodeConfiguration(), TbSendRestApiCallReplyNode.class.getName(), new TbAwsLambdaNodeConfiguration(), TbAwsLambdaNode.class.getName() ); - @Test - public void testRuleChainMetadataUpdateMsgForOldEdgeVersions() { + @ParameterizedTest(name = "Testing metadata update for EdgeVersion: {0}") + @EnumSource(value = EdgeVersion.class, names = {"V_4_0_0", "V_3_9_0", "V_3_8_0", "V_3_7_0"}) + @DisplayName("Test RuleChain Metadata Update for Supported Edge Versions") + public void testRuleChainMetadataUpdateForSupportedEdgeVersions(EdgeVersion edgeVersion) { // GIVEN - RuleChainMetaData metaData = createMetadataWithProblemNodes(CONFIG_TO_NODE_NAME); + RuleChainMetaData metaData = createMetadataWithNodes(NODE_CONFIG_TO_NAME_MAP); - SUPPORTED_EDGE_VERSIONS_FOR_TESTS.forEach(edgeVersion -> { - // WHEN - List ruleNodes = extractRuleNodesFromUpdateMsg(metaData, edgeVersion); + // WHEN + List ruleNodes = extractRuleNodesFromMetadata(metaData, edgeVersion); - // THEN - assertRuleNodeConfig(ruleNodes, edgeVersion); - }); + // THEN + verifyRuleNodeConfigurations(ruleNodes, edgeVersion); } - @Test - public void testRuleChainMetadataWithMissingNodeForOldEdgeVersions() { + @ParameterizedTest(name = "Testing metadata with missing nodes for EdgeVersion: {0}") + @EnumSource(value = EdgeVersion.class, names = {"V_4_0_0", "V_3_9_0", "V_3_8_0", "V_3_7_0"}) + @DisplayName("Test RuleChain Metadata with Missing Nodes for Old Edge Versions") + public void testRuleChainMetadataWithMissingNodesForOldEdgeVersions(EdgeVersion edgeVersion) { // GIVEN - RuleChainMetaData metaData = createMetadataWithProblemNodes(CONFIG_TO_MISS_NODE_FOR_OLD_EDGE); - - SUPPORTED_EDGE_VERSIONS_FOR_TESTS.forEach(edgeVersion -> { - // WHEN - List ruleNodes = extractRuleNodesFromUpdateMsg(metaData, edgeVersion); - - // THEN - int leftNode = EXCLUDED_NODES_BY_EDGE_VERSION.containsKey(edgeVersion) ? - CONFIG_TO_MISS_NODE_FOR_OLD_EDGE.size() - EXCLUDED_NODES_BY_EDGE_VERSION.get(edgeVersion).size() : - CONFIG_TO_MISS_NODE_FOR_OLD_EDGE.size(); - - Assert.assertEquals(leftNode, ruleNodes.size()); - }); + RuleChainMetaData metaData = createMetadataWithNodes(MISSING_NODE_CONFIGS_FOR_OLD_EDGES); + + // WHEN + List ruleNodes = extractRuleNodesFromMetadata(metaData, edgeVersion); + + // THEN + int expectedNodeCount = EXCLUDED_NODES_BY_EDGE_VERSION.containsKey(edgeVersion) ? + MISSING_NODE_CONFIGS_FOR_OLD_EDGES.size() - EXCLUDED_NODES_BY_EDGE_VERSION.get(edgeVersion).size() : + MISSING_NODE_CONFIGS_FOR_OLD_EDGES.size(); + Assertions.assertEquals( + expectedNodeCount, + ruleNodes.size(), + String.format("EdgeVersion '%s' should have %d nodes, but found %d.", edgeVersion, expectedNodeCount, ruleNodes.size()) + ); } - private RuleChainMetaData createMetadataWithProblemNodes(Map nodeMap) { + private RuleChainMetaData createMetadataWithNodes(Map nodeConfigMap) { RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); List ruleNodes = new ArrayList<>(); - nodeMap.forEach((key, value) -> { + nodeConfigMap.forEach((config, nodeName) -> { RuleNode ruleNode = new RuleNode(); - - ruleNode.setName(value); - ruleNode.setType(value); + ruleNode.setName(nodeName); + ruleNode.setType(nodeName); ruleNode.setConfigurationVersion(CONFIGURATION_VERSION); - ruleNode.setConfiguration(JacksonUtil.valueToTree(key.defaultConfiguration())); - + ruleNode.setConfiguration(JacksonUtil.valueToTree(config.defaultConfiguration())); ruleNodes.add(ruleNode); }); ruleChainMetaData.setFirstNodeIndex(0); ruleChainMetaData.setNodes(ruleNodes); - return ruleChainMetaData; } - private List extractRuleNodesFromUpdateMsg(RuleChainMetaData metaData, EdgeVersion edgeVersion) { - String ruleChainMetadataUpdateMsg = - EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, metaData, edgeVersion).getEntity(); + private List extractRuleNodesFromMetadata(RuleChainMetaData metaData, EdgeVersion edgeVersion) { + String metadataUpdateMsg = EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg( + UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, + metaData, + edgeVersion + ).getEntity(); - RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg, RuleChainMetaData.class, true); - - Assert.assertNotNull("RuleChainMetaData is null", ruleChainMetaData); - - return ruleChainMetaData.getNodes(); + RuleChainMetaData updatedMetaData = JacksonUtil.fromString(metadataUpdateMsg, RuleChainMetaData.class, true); + Assertions.assertNotNull(updatedMetaData, "RuleChainMetaData should not be null after update."); + return updatedMetaData.getNodes(); } - private void assertRuleNodeConfig(List ruleNodes, EdgeVersion edgeVersion) { + private void verifyRuleNodeConfigurations(List ruleNodes, EdgeVersion edgeVersion) { ruleNodes.forEach(ruleNode -> { - int configParamCount = NODE_TO_CONFIG_PARAMS_COUNT.get(ruleNode.getType()); + String nodeType = ruleNode.getType(); + int expectedParamCount = NODE_NAME_TO_CONFIG_PARAM_COUNT_MAP.getOrDefault(nodeType, 0); + + boolean isRuleNodeModified = IGNORED_PARAMS_BY_EDGE_VERSION + .getOrDefault(edgeVersion, Map.of()) + .containsKey(nodeType); - boolean isOldEdgeVersion = IGNORED_PARAMS_BY_EDGE_VERSION.entrySet().stream() - .anyMatch(entry -> entry.getKey().equals(edgeVersion) && - entry.getValue().containsKey(ruleNode.getType())); - int expectedConfigAmount = isOldEdgeVersion ? configParamCount - 1 : configParamCount; + int actualParamCount = isRuleNodeModified ? expectedParamCount - 1 : expectedParamCount; - Assert.assertEquals( - String.format("For ruleNode '%s', edgeVersion '%s", ruleNode.getName(), edgeVersion), - expectedConfigAmount, ruleNode.getConfiguration().size() + Assertions.assertEquals( + actualParamCount, + ruleNode.getConfiguration().size(), + String.format("RuleNode '%s' for EdgeVersion '%s' should have %d config parameters.", + ruleNode.getName(), edgeVersion, actualParamCount) ); }); } From ba238d61f3d655362e2aaf7c30ec09dd9742ec2d Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 31 Mar 2025 15:23:16 +0300 Subject: [PATCH 20/54] fixed issue with comparing different data point types --- .../thingsboard/server/edqs/data/dp/BoolDataPoint.java | 7 ++++++- .../thingsboard/server/edqs/data/dp/DoubleDataPoint.java | 6 +++++- .../thingsboard/server/edqs/data/dp/LongDataPoint.java | 8 +++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java index 70a14917ba..97a0a265d5 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java @@ -16,6 +16,7 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; +import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.edqs.DataPoint; import org.thingsboard.server.common.data.kv.DataType; @@ -46,6 +47,10 @@ public class BoolDataPoint extends AbstractDataPoint { @Override public int compareTo(DataPoint dataPoint) { - return Boolean.compare(value, dataPoint.getBool()); + if (dataPoint.getType() == DataType.BOOLEAN) { + return StringUtils.compareIgnoreCase(valueToString(), dataPoint.valueToString()); + } else { + return super.compareTo(dataPoint); + } } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java index 2ca8f2c03a..4660b3eb56 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/DoubleDataPoint.java @@ -46,6 +46,10 @@ public class DoubleDataPoint extends AbstractDataPoint { @Override public int compareTo(DataPoint dataPoint) { - return Double.compare(value, dataPoint.getDouble()); + if (dataPoint.getType() == DataType.DOUBLE || dataPoint.getType() == DataType.LONG) { + return Double.compare(value, dataPoint.getDouble()); + } else { + return super.compareTo(dataPoint); + } } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java index 92c0a972e7..a1d33203b7 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/LongDataPoint.java @@ -51,6 +51,12 @@ public class LongDataPoint extends AbstractDataPoint { @Override public int compareTo(DataPoint dataPoint) { - return Long.compare(value, dataPoint.getLong()); + if (dataPoint.getType() == DataType.DOUBLE) { + return Double.compare(getDouble(), dataPoint.getDouble()); + } else if (dataPoint.getType() == DataType.LONG) { + return Long.compare(value, dataPoint.getLong()); + } else { + return super.compareTo(dataPoint); + } } } From ea726d0147e2364324c94bd465cf755c1d354ddd Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 1 Apr 2025 12:16:54 +0300 Subject: [PATCH 21/54] UI: Fixed positioning for color picker popover --- ui-ngx/src/app/shared/components/color-input.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/color-input.component.ts b/ui-ngx/src/app/shared/components/color-input.component.ts index d0dc7235f7..77d02b47d3 100644 --- a/ui-ngx/src/app/shared/components/color-input.component.ts +++ b/ui-ngx/src/app/shared/components/color-input.component.ts @@ -201,7 +201,7 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro renderer: this.renderer, hostView: this.viewContainerRef, componentType: ColorPickerPanelComponent, - preferredPlacement: ['left'], + preferredPlacement: ['leftTopOnly', 'leftOnly', 'leftBottomOnly'], context: { color: this.colorFormGroup.get('color').value, colorClearButton: this.colorClearButton, From 665bb03a1fd7acf8dd80595c7cee607fa072a0df Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 1 Apr 2025 12:19:11 +0300 Subject: [PATCH 22/54] non-exiting data point should return 0 ts --- .../java/org/thingsboard/server/edqs/util/RepositoryUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java index 71b58d759a..f355d83e62 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/RepositoryUtils.java @@ -344,7 +344,7 @@ public class RepositoryUtils { if (dp != null) { return new TsValue(dp.getTs() > 0 ? dp.getTs() : ts, dp.valueToString()); } else { - return new TsValue(ts, ""); + return TsValue.EMPTY; } } From bca508ac00aade7b23e01875b7d8f71b32ed5832 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 1 Apr 2025 13:23:28 +0300 Subject: [PATCH 23/54] added test --- .../server/controller/EntityQueryControllerTest.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 90b7ea7c7b..29a208a805 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -499,7 +499,8 @@ public class EntityQueryControllerTest extends AbstractControllerTest { ); EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); List entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); - List latestValues = Collections.singletonList(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); + List latestValues = List.of(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"), + new EntityKey(EntityKeyType.ATTRIBUTE, "non-existing-attribute")); EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, null); PageData data = findByQueryAndCheck(query, 67); @@ -517,6 +518,14 @@ public class EntityQueryControllerTest extends AbstractControllerTest { List deviceTemperatures = temperatures.stream().map(aLong -> Long.toString(aLong)).collect(Collectors.toList()); Assert.assertEquals(deviceTemperatures, loadedTemperatures); + // check ts value == 0, value is empty string for non-existing data points + List loadedNonExistingAttributes = loadedEntities.stream().map(entityData -> + entityData.getLatest().get(EntityKeyType.ATTRIBUTE).get("non-existing-attribute")).toList(); + loadedNonExistingAttributes.forEach(tsValue -> { + assertThat(tsValue.getTs()).isEqualTo(0L); + assertThat(tsValue.getValue()).isEqualTo(""); + }); + pageLink = new EntityDataPageLink(10, 0, null, sortOrder); KeyFilter highTemperatureFilter = new KeyFilter(); highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature")); From 3cefb634367dd3309760457a38e3ef2522cbca83 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 1 Apr 2025 14:57:36 +0300 Subject: [PATCH 24/54] UI: Add to place map item action customize tooltip --- ui-ngx/src/app/core/services/utils.service.ts | 2 +- .../home/components/widget/lib/maps/map.ts | 41 ++++-- .../action/map-item-tooltips.component.html | 82 ++++++++++++ .../action/map-item-tooltips.component.ts | 123 ++++++++++++++++++ .../action/widget-action.component.html | 4 + .../common/action/widget-action.component.ts | 4 +- .../common/widget-settings-common.module.ts | 4 + ui-ngx/src/app/shared/models/widget.models.ts | 25 +++- .../assets/locale/locale.constant-en_US.json | 13 +- 9 files changed, 285 insertions(+), 13 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.ts diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index 61f16d1f67..bfaad1148d 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -208,7 +208,7 @@ export class UtilsService { return parseException(exception, lineOffset); } - public customTranslation(translationValue: string, defaultValue: string): string { + public customTranslation(translationValue: string, defaultValue: string = translationValue): string { if (translationValue && isString(translationValue)) { if (translationValue.includes(`{${i18nPrefix}`)) { const matches = translationValue.match(i18nRegExp); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts index f1bb414878..24a02c6875 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts @@ -50,7 +50,14 @@ import { UnplacedMapDataItem, } from '@home/components/widget/lib/maps/data-layer/latest-map-data-layer'; import { IWidgetSubscription, PlaceMapItemActionData, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; -import { FormattedData, MapItemType, WidgetAction, WidgetActionType, widgetType } from '@shared/models/widget.models'; +import { + FormattedData, + mapItemTooltipsTranslation, + MapItemType, + WidgetAction, + WidgetActionType, + widgetType +} from '@shared/models/widget.models'; import { EntityDataPageLink } from '@shared/models/query/query.models'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import { TbMarkersDataLayer } from '@home/components/widget/lib/maps/data-layer/markers-data-layer'; @@ -661,29 +668,45 @@ export abstract class TbMap { private createMarker(actionData: PlaceMapItemActionData) { this.createItem(actionData, () => this.prepareDrawMode('Marker', { - placeMarker: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker-hint') + placeMarker: actionData.action.mapItemTooltips.placeMarker + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.placeMarker) + : this.ctx.translate.instant(mapItemTooltipsTranslation.placeMarker) })); } private createRectangle(actionData: PlaceMapItemActionData): void { this.createItem(actionData, () => this.prepareDrawMode('Rectangle', { - firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polygon.rectangle-place-first-point-hint'), - finishRect: this.ctx.translate.instant('widgets.maps.data-layer.polygon.finish-rectangle-hint') + firstVertex: actionData.action.mapItemTooltips.startRect + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.startRect) + : this.ctx.translate.instant(mapItemTooltipsTranslation.startRect), + finishRect: actionData.action.mapItemTooltips.finishRect + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.finishRect) + : this.ctx.translate.instant(mapItemTooltipsTranslation.finishRect), })); } private createPolygon(actionData: PlaceMapItemActionData): void { this.createItem(actionData, () => this.prepareDrawMode('Polygon', { - firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polygon.polygon-place-first-point-hint'), - continueLine: this.ctx.translate.instant('widgets.maps.data-layer.polygon.continue-polygon-hint'), - finishPoly: this.ctx.translate.instant('widgets.maps.data-layer.polygon.finish-polygon-hint') + firstVertex: actionData.action.mapItemTooltips.firstVertex + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.firstVertex) + : this.ctx.translate.instant(mapItemTooltipsTranslation.firstVertex), + continueLine: actionData.action.mapItemTooltips.continueLine + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.continueLine) + : this.ctx.translate.instant(mapItemTooltipsTranslation.continueLine), + finishPoly: actionData.action.mapItemTooltips.finishPoly + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.finishPoly) + : this.ctx.translate.instant(mapItemTooltipsTranslation.finishPoly), })); } private createCircle(actionData: PlaceMapItemActionData): void { this.createItem(actionData, () => this.prepareDrawMode('Circle', { - startCircle: this.ctx.translate.instant('widgets.maps.data-layer.circle.place-circle-center-hint'), - finishCircle: this.ctx.translate.instant('widgets.maps.data-layer.circle.finish-circle-hint') + startCircle: actionData.action.mapItemTooltips.startCircle + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.startCircle) + : this.ctx.translate.instant(mapItemTooltipsTranslation.startCircle), + finishCircle: actionData.action.mapItemTooltips.finishCircle + ? this.ctx.utilsService.customTranslation(actionData.action.mapItemTooltips.finishCircle) + : this.ctx.translate.instant(mapItemTooltipsTranslation.finishCircle), })); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html new file mode 100644 index 0000000000..1ef16f9232 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html @@ -0,0 +1,82 @@ + +
+ + {{ 'widget-action.map-item-tooltip.tooltips' | translate }} + + @switch (mapItemType) { + @case (MapItemType.marker) { +
+
widget-action.map-item-tooltip.place-marker
+ + + +
+ } + @case (MapItemType.rectangle) { +
+
widget-action.map-item-tooltip.start-draw-rectangle
+ + + +
+
+
widget-action.map-item-tooltip.finish-draw-rectangle
+ + + +
+ } + @case (MapItemType.polygon) { +
+
widget-action.map-item-tooltip.start-draw-polygon
+ + + +
+
+
widget-action.map-item-tooltip.continue-draw-polygon
+ + + +
+
+
widget-action.map-item-tooltip.finish-draw-polygon
+ + + +
+ } + @case (MapItemType.circle) { +
+
widget-action.map-item-tooltip.start-draw-circle
+ + + +
+
+
widget-action.map-item-tooltip.finish-draw-circle
+ + + +
+ } + } +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.ts new file mode 100644 index 0000000000..830fd925b7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.ts @@ -0,0 +1,123 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { MapItemTooltips, MapItemType, mapItemTooltipsTranslation } from '@shared/models/widget.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { deepTrim, isEqual } from '@core/utils'; + +@Component({ + selector: 'tb-map-item-tooltips', + templateUrl: './map-item-tooltips.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MapItemTooltipsComponent), + multi: true + } + ] +}) +export class MapItemTooltipsComponent implements ControlValueAccessor, OnChanges { + + @Input({required: true}) + mapItemType: MapItemType; + + tooltipsForm: FormGroup; + MapItemType = MapItemType; + readonly mapItemTooltipsDefaultTranslate = mapItemTooltipsTranslation; + + private modelValue: MapItemTooltips; + private propagateChange = (_val: any) => {}; + + constructor(private fd: FormBuilder) { + this.tooltipsForm = this.fd.group({ + placeMarker: [''], + firstVertex: [''], + continueLine: [''], + finishPoly: [''], + startRect: [''], + finishRect: [''], + startCircle: [''], + finishCircle: [''] + }); + + this.tooltipsForm.valueChanges.pipe( + takeUntilDestroyed() + ).subscribe(this.updatedModel.bind(this)); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.mapItemType) { + const mapItemTypeChanges = changes.mapItemType; + if (!mapItemTypeChanges.firstChange && mapItemTypeChanges.currentValue !== mapItemTypeChanges.previousValue) { + this.updatedValidators(true); + } + } + } + + registerOnChange(fn: any) { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any) { + } + + setDisabledState(isDisabled: boolean) { + if (isDisabled) { + this.tooltipsForm.disable({emitEvent: false}); + } else { + this.tooltipsForm.enable({emitEvent: false}); + } + } + + writeValue(obj: MapItemTooltips) { + this.modelValue = obj; + this.tooltipsForm.patchValue(obj, {emitEvent: false}); + this.updatedValidators(); + } + + private updatedValidators(emitNewValue = false) { + this.tooltipsForm.disable({emitEvent: false}); + switch (this.mapItemType) { + case MapItemType.marker: + this.tooltipsForm.get('placeMarker').enable({emitEvent: false}); + break; + case MapItemType.rectangle: + this.tooltipsForm.get('startRect').enable({emitEvent: false}); + this.tooltipsForm.get('finishRect').enable({emitEvent: false}); + break; + case MapItemType.polygon: + this.tooltipsForm.get('firstVertex').enable({emitEvent: false}); + this.tooltipsForm.get('continueLine').enable({emitEvent: false}); + this.tooltipsForm.get('finishPoly').enable({emitEvent: false}); + break; + case MapItemType.circle: + this.tooltipsForm.get('startCircle').enable({emitEvent: false}); + this.tooltipsForm.get('finishCircle').enable({emitEvent: false}); + break; + } + this.tooltipsForm.updateValueAndValidity({emitEvent: emitNewValue}) + } + + private updatedModel(value: MapItemTooltips) { + const currentValue = Object.fromEntries(Object.entries(deepTrim(value)).filter(([_, v]) => v != '')); + if (!isEqual(currentValue, this.modelValue)) { + this.modelValue = currentValue; + this.propagateChange(currentValue); + } + } +} 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 5c735010f1..a58b58bb45 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 @@ -270,6 +270,10 @@ + + Date: Tue, 1 Apr 2025 15:42:32 +0300 Subject: [PATCH 25/54] UI: Fixed translation --- .../lib/settings/common/action/map-item-tooltips.component.html | 2 +- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html index 1ef16f9232..f0abe17509 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/map-item-tooltips.component.html @@ -17,7 +17,7 @@ -->
- {{ 'widget-action.map-item-tooltip.tooltips' | translate }} + {{ 'widget-action.map-item-tooltip.customize-map-item-tooltips' | translate }} @switch (mapItemType) { @case (MapItemType.marker) { 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 f312b7a8d2..813ab7709a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6594,7 +6594,7 @@ }, "place-map-item": "Place map item", "map-item-tooltip": { - "tooltips": "Map item tooltips", + "customize-map-item-tooltips": "Customize map item tooltips", "place-marker": "Place marker", "start-draw-rectangle": "Start draw rectangle", "finish-draw-rectangle": "Finish draw rectangle", From dbd74a8b4c54d1a9ed4e413d0ccc80a1972e3f57 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 1 Apr 2025 16:20:50 +0300 Subject: [PATCH 26/54] added test --- .../service/entitiy/EntityServiceTest.java | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java index e5c4009715..a2c3fc07c8 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/EntityServiceTest.java @@ -95,7 +95,6 @@ import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewDao; import org.thingsboard.server.dao.entityview.EntityViewService; -import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.sql.relation.RelationRepository; @@ -1664,7 +1663,7 @@ public class EntityServiceTest extends AbstractControllerTest { List> timeseriesFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { Device device = devices.get(i); - timeseriesFutures.add(saveLongTimeseries(device.getId(), "temperature", temperatures.get(i))); + timeseriesFutures.add(saveTimeseries(device.getId(), "temperature", temperatures.get(i))); } Futures.allAsList(timeseriesFutures).get(); @@ -1711,6 +1710,13 @@ public class EntityServiceTest extends AbstractControllerTest { List entitiesTelemetry = loadedEntities.stream().map(entityData -> entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList(); assertThat(entitiesTelemetry).containsExactlyElementsOf(expectedSortedList); + // update temperature to long value for one of device + long longTempValue = -100L; + saveTimeseries(devices.get(new Random().nextInt(66)).getId(), "temperature", longTempValue).get(); + loadedEntities = loadAllData(querySortByTemp, deviceTemperatures.size()); + entitiesTelemetry = loadedEntities.stream().map(entityData -> entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList(); + assertThat(entitiesTelemetry.get(0)).isEqualTo(String.valueOf(longTempValue)); + deviceService.deleteDevicesByTenantId(tenantId); } @@ -2252,15 +2258,18 @@ public class EntityServiceTest extends AbstractControllerTest { return attributesService.save(tenantId, entityId, scope, Collections.singletonList(attr)); } - private ListenableFuture saveLongTimeseries(EntityId entityId, String key, Double value) { - TsKvEntity tsKv = new TsKvEntity(); - tsKv.setStrKey(key); - tsKv.setDoubleValue(value); + private ListenableFuture saveTimeseries(EntityId entityId, String key, Double value) { KvEntry telemetryValue = new DoubleDataEntry(key, value); BasicTsKvEntry timeseries = new BasicTsKvEntry(42L, telemetryValue); return timeseriesService.save(tenantId, entityId, timeseries); } + private ListenableFuture saveTimeseries(EntityId entityId, String key, Long value) { + KvEntry telemetryValue = new LongDataEntry(key, value); + BasicTsKvEntry timeseries = new BasicTsKvEntry(42L, telemetryValue); + return timeseriesService.save(tenantId, entityId, timeseries); + } + protected void createMultiRootHierarchy(List buildings, List apartments, Map> entityNameByTypeMap, Map childParentRelationMap) throws InterruptedException { From cf405c32e78215686341d553c2028bd6b65605fd Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 1 Apr 2025 16:37:31 +0300 Subject: [PATCH 27/54] fixed BoolDataPoint compare method --- .../org/thingsboard/server/edqs/data/dp/BoolDataPoint.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java index 97a0a265d5..adb03a5cb1 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/BoolDataPoint.java @@ -16,7 +16,6 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; -import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.edqs.DataPoint; import org.thingsboard.server.common.data.kv.DataType; @@ -48,7 +47,7 @@ public class BoolDataPoint extends AbstractDataPoint { @Override public int compareTo(DataPoint dataPoint) { if (dataPoint.getType() == DataType.BOOLEAN) { - return StringUtils.compareIgnoreCase(valueToString(), dataPoint.valueToString()); + return Boolean.compare(value, dataPoint.getBool()); } else { return super.compareTo(dataPoint); } From bfb475744069614f78ffed25c3ce25ab33eee86d Mon Sep 17 00:00:00 2001 From: yevhenii Date: Tue, 1 Apr 2025 16:50:04 +0300 Subject: [PATCH 28/54] Fix RuleChainMetadata for older Edge versions - refactoring test --- .../edge/EdgeMsgConstructorUtilsTest.java | 171 +++++++++--------- 1 file changed, 84 insertions(+), 87 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index ea82ec1780..a4c2133f1c 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -16,34 +16,34 @@ package org.thingsboard.server.service.edge; import lombok.extern.slf4j.Slf4j; -import org.junit.Assert; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode; -import org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNodeConfiguration; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNode; -import org.thingsboard.rule.engine.aws.lambda.TbAwsLambdaNodeConfiguration; +import org.thingsboard.rule.engine.filter.TbCheckRelationNode; +import org.thingsboard.rule.engine.flow.TbAckNode; +import org.thingsboard.rule.engine.math.TbMathNode; +import org.thingsboard.rule.engine.metadata.CalculateDeltaNode; +import org.thingsboard.rule.engine.metadata.TbGetTelemetryNode; import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode; -import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNodeConfiguration; import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode; -import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNodeConfiguration; import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; -import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; -import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; -import java.util.ArrayList; -import java.util.HashMap; +import java.lang.reflect.Constructor; import java.util.List; -import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.EXCLUDED_NODES_BY_EDGE_VERSION; import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.IGNORED_PARAMS_BY_EDGE_VERSION; @@ -51,109 +51,106 @@ import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.IGNORE @Slf4j @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class EdgeMsgConstructorUtilsTest { - private static final int CONFIGURATION_VERSION = 5; - - private static final Map NODE_CONFIG_TO_NAME_MAP = Map.of( - new TbMsgTimeseriesNodeConfiguration(), TbMsgTimeseriesNode.class.getName(), - new TbMsgAttributesNodeConfiguration(), TbMsgAttributesNode.class.getName(), - new TbSaveToCustomCassandraTableNodeConfiguration(), TbSaveToCustomCassandraTableNode.class.getName() - ); - private static final Map NODE_NAME_TO_CONFIG_PARAM_COUNT_MAP = Map.of( - TbMsgTimeseriesNode.class.getName(), 3, - TbMsgAttributesNode.class.getName(), 5, - TbSaveToCustomCassandraTableNode.class.getName(), 3 - ); + private static final int CONFIGURATION_VERSION = 5; + static Stream provideEdgeVersions() { + return Stream.of( + EdgeVersion.V_4_0_0, + EdgeVersion.V_3_9_0, + EdgeVersion.V_3_8_0, + EdgeVersion.V_3_7_0 + ); + } - private static final Map MISSING_NODE_CONFIGS_FOR_OLD_EDGES = Map.of( - new TbSendRestApiCallReplyNodeConfiguration(), TbSendRestApiCallReplyNode.class.getName(), - new TbAwsLambdaNodeConfiguration(), TbAwsLambdaNode.class.getName() - ); + private static final RuleChainMetaData RULE_CHAIN_META_DATA = new RuleChainMetaData(); + private static final List TEST_NODES = + List.of( + new TbSaveToCustomCassandraTableNode(), + new TbMsgAttributesNode(), + new TbMsgTimeseriesNode(), + new TbSendRestApiCallReplyNode(), + new TbAwsLambdaNode(), + + new TbMathNode(), + new CalculateDeltaNode(), + new TbAckNode(), + new TbCheckRelationNode(), + new TbGetTelemetryNode() + ); - @ParameterizedTest(name = "Testing metadata update for EdgeVersion: {0}") - @EnumSource(value = EdgeVersion.class, names = {"V_4_0_0", "V_3_9_0", "V_3_8_0", "V_3_7_0"}) - @DisplayName("Test RuleChain Metadata Update for Supported Edge Versions") - public void testRuleChainMetadataUpdateForSupportedEdgeVersions(EdgeVersion edgeVersion) { - // GIVEN - RuleChainMetaData metaData = createMetadataWithNodes(NODE_CONFIG_TO_NAME_MAP); + @BeforeAll + static void setUp() { + List ruleNodes = TEST_NODES.stream() + .map(node -> { + RuleNode ruleNode = new RuleNode(); + ruleNode.setName(node.getClass().getName()); + ruleNode.setType(node.getClass().getName()); + ruleNode.setConfigurationVersion(CONFIGURATION_VERSION); + ruleNode.setConfiguration(JacksonUtil.valueToTree(createDefaultConfiguration(node))); + return ruleNode; + }) + .toList(); + + RULE_CHAIN_META_DATA.setFirstNodeIndex(0); + RULE_CHAIN_META_DATA.setNodes(ruleNodes); + } - // WHEN - List ruleNodes = extractRuleNodesFromMetadata(metaData, edgeVersion); + private static NodeConfiguration createDefaultConfiguration(TbNode node) { + try { + org.thingsboard.rule.engine.api.RuleNode annotation = node.getClass().getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class); + Constructor constructor = annotation.configClazz().getConstructor(); + NodeConfiguration configInstance = (NodeConfiguration) constructor.newInstance(); - // THEN - verifyRuleNodeConfigurations(ruleNodes, edgeVersion); + return configInstance.defaultConfiguration(); + } catch (Exception e) { + throw new RuntimeException("Exception during creating RuleNodeConfiguration for node - " + node, e); + } } - @ParameterizedTest(name = "Testing metadata with missing nodes for EdgeVersion: {0}") - @EnumSource(value = EdgeVersion.class, names = {"V_4_0_0", "V_3_9_0", "V_3_8_0", "V_3_7_0"}) - @DisplayName("Test RuleChain Metadata with Missing Nodes for Old Edge Versions") - public void testRuleChainMetadataWithMissingNodesForOldEdgeVersions(EdgeVersion edgeVersion) { - // GIVEN - RuleChainMetaData metaData = createMetadataWithNodes(MISSING_NODE_CONFIGS_FOR_OLD_EDGES); - + @ParameterizedTest(name = "Test Sanitize Metadata For Edge: {0}") + @MethodSource("provideEdgeVersions") + @DisplayName("Test Sanitize Metadata For Legacy Edge Version") + public void testSanitizeMetadataForLegacyEdgeVersion(EdgeVersion edgeVersion) { // WHEN - List ruleNodes = extractRuleNodesFromMetadata(metaData, edgeVersion); + List ruleNodes = sanitizeMetadataForLegacyEdgeVersion(edgeVersion); // THEN - int expectedNodeCount = EXCLUDED_NODES_BY_EDGE_VERSION.containsKey(edgeVersion) ? - MISSING_NODE_CONFIGS_FOR_OLD_EDGES.size() - EXCLUDED_NODES_BY_EDGE_VERSION.get(edgeVersion).size() : - MISSING_NODE_CONFIGS_FOR_OLD_EDGES.size(); - Assertions.assertEquals( - expectedNodeCount, - ruleNodes.size(), - String.format("EdgeVersion '%s' should have %d nodes, but found %d.", edgeVersion, expectedNodeCount, ruleNodes.size()) - ); - } - - private RuleChainMetaData createMetadataWithNodes(Map nodeConfigMap) { - RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); - List ruleNodes = new ArrayList<>(); - - nodeConfigMap.forEach((config, nodeName) -> { - RuleNode ruleNode = new RuleNode(); - ruleNode.setName(nodeName); - ruleNode.setType(nodeName); - ruleNode.setConfigurationVersion(CONFIGURATION_VERSION); - ruleNode.setConfiguration(JacksonUtil.valueToTree(config.defaultConfiguration())); - ruleNodes.add(ruleNode); + ruleNodes.forEach(ruleNode -> { + checkUpdateNodeConfigurationsForLegacyEdge(ruleNode, edgeVersion); + checkRemoveExcludedNodesForLegacyEdge(ruleNode, edgeVersion); }); - - ruleChainMetaData.setFirstNodeIndex(0); - ruleChainMetaData.setNodes(ruleNodes); - return ruleChainMetaData; } - private List extractRuleNodesFromMetadata(RuleChainMetaData metaData, EdgeVersion edgeVersion) { + private List sanitizeMetadataForLegacyEdgeVersion(EdgeVersion edgeVersion) { String metadataUpdateMsg = EdgeMsgConstructorUtils.constructRuleChainMetadataUpdatedMsg( UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, - metaData, + RULE_CHAIN_META_DATA, edgeVersion ).getEntity(); RuleChainMetaData updatedMetaData = JacksonUtil.fromString(metadataUpdateMsg, RuleChainMetaData.class, true); Assertions.assertNotNull(updatedMetaData, "RuleChainMetaData should not be null after update."); + return updatedMetaData.getNodes(); } - private void verifyRuleNodeConfigurations(List ruleNodes, EdgeVersion edgeVersion) { - ruleNodes.forEach(ruleNode -> { - String nodeType = ruleNode.getType(); - int expectedParamCount = NODE_NAME_TO_CONFIG_PARAM_COUNT_MAP.getOrDefault(nodeType, 0); + private void checkUpdateNodeConfigurationsForLegacyEdge(RuleNode ruleNode, EdgeVersion edgeVersion) { + if (IGNORED_PARAMS_BY_EDGE_VERSION.containsKey(edgeVersion) && IGNORED_PARAMS_BY_EDGE_VERSION.get(edgeVersion).containsKey(ruleNode.getType())) { + String ignoredParam = IGNORED_PARAMS_BY_EDGE_VERSION.get(edgeVersion).get(ruleNode.getType()); - boolean isRuleNodeModified = IGNORED_PARAMS_BY_EDGE_VERSION - .getOrDefault(edgeVersion, Map.of()) - .containsKey(nodeType); + Assertions.assertFalse(ruleNode.getConfiguration().has(ignoredParam), + String.format("RuleNode '%s' for EdgeVersion '%s' should ignore '%s' config parameter.", ruleNode.getName(), edgeVersion, ignoredParam)); + } + } - int actualParamCount = isRuleNodeModified ? expectedParamCount - 1 : expectedParamCount; + private void checkRemoveExcludedNodesForLegacyEdge(RuleNode ruleNode, EdgeVersion edgeVersion) { + boolean isNodeExcluded = Optional.ofNullable(EXCLUDED_NODES_BY_EDGE_VERSION.get(edgeVersion)) + .map(excludedNodes -> !excludedNodes.contains(ruleNode.getType())) + .orElse(true); - Assertions.assertEquals( - actualParamCount, - ruleNode.getConfiguration().size(), - String.format("RuleNode '%s' for EdgeVersion '%s' should have %d config parameters.", - ruleNode.getName(), edgeVersion, actualParamCount) - ); - }); + Assertions.assertTrue(isNodeExcluded, + String.format("For EdgeVersion '%s', ruleNode '%s' should not be included.", edgeVersion, ruleNode.getType())); } } From a043ca68ad241c5fbd3182db2afa080db99cccda Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 1 Apr 2025 17:43:49 +0300 Subject: [PATCH 29/54] UI: New map widget - refactor create tooltip in order to control layer interaction (including leaflet-geoman ugly behavior: disable/enable popups on toggle global draw mode) --- .../lib/maps/data-layer/data-layer-utils.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts index d5329586be..4b7b933d74 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts @@ -29,35 +29,39 @@ export const createTooltip = (map: TbMap, settings: DataLayerTooltipSettings, data: FormattedData, canOpen: () => boolean): L.Popup => { - const tooltip = L.popup(); - layer.bindPopup(tooltip, {autoClose: settings.autoclose, closeOnClick: false}); - layer.off('click'); + const tooltip = L.popup({autoClose: settings.autoclose, closeOnClick: false}); + (tooltip as any)._source = layer; + layer.on('move', (e) => { + tooltip.setLatLng((e as any).latlng); + }); + layer.on('remove', () => { + tooltip.close(); + }); if (settings.trigger === DataLayerTooltipTrigger.click) { - layer.on('click', () => { + layer.on('click', (e) => { + L.DomEvent.stop(e); if (tooltip.isOpen()) { - layer.closePopup(); + tooltip.close(); } else if (canOpen()) { - layer.openPopup(); + if ((tooltip as any)._prepareOpen((layer as any)._latlng)) { + tooltip.openOn(map.getMap()); + } } }); } else if (settings.trigger === DataLayerTooltipTrigger.hover) { layer.on('mouseover', () => { if (canOpen()) { - layer.openPopup(); + if ((tooltip as any)._prepareOpen((layer as any)._latlng)) { + tooltip.openOn(map.getMap()); + } } }); - layer.on('mousemove', (e) => { - tooltip.setLatLng(e.latlng); - }); layer.on('mouseout', () => { - layer.closePopup(); + tooltip.close(); }); } layer.on('popupopen', () => { bindTooltipActions(map, tooltip, settings, data); - (layer as any)._popup._closeButton.addEventListener('click', (event: Event) => { - event.preventDefault(); - }); }); return tooltip; } From 63649046ed31fee0a4839f5441e9f944226a89e8 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 1 Apr 2025 18:20:30 +0300 Subject: [PATCH 30/54] UI: Fixed typo in 'Calculate Field' script section title --- .../components/dialog/calculated-field-dialog.component.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 813f6b1b4c..1688685523 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -75,7 +75,9 @@ />
-
{{ 'calculated-fields.expression' | translate }}
+
+ {{ (fieldFormGroup.get('type').value === CalculatedFieldType.SIMPLE ? 'calculated-fields.expression' : 'calculated-fields.type.script' ) | translate }} +
@if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) { From 390a10bdb744329365f0e22024c9937c522a1833 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 1 Apr 2025 18:36:05 +0300 Subject: [PATCH 31/54] UI: Fixed time-unit component not correctly calculating selected time --- .../components/rule-node/common/time-unit-input.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts index 54bb7fa9b8..b0d0a97641 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/time-unit-input.component.ts @@ -177,7 +177,7 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator, } private updatedModel(value: Partial, forceUpdated = false) { - const time = value.time * this.timeIntervalsInSec.get(value.timeUnit); + const time = isDefinedAndNotNull(value.time) ? value.time * this.timeIntervalsInSec.get(value.timeUnit) : null; if (this.modelValue !== time || forceUpdated) { this.modelValue = time; this.propagateChange(time); From bffa565842b38423dd65350bf97d1665a309ba4f Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 2 Apr 2025 13:04:23 +0300 Subject: [PATCH 32/54] UI: Add fetch function for string items list; fix double item addition on blur --- .../string-items-list.component.html | 16 +++---- .../components/string-items-list.component.ts | 46 +++++++++++++------ 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.html b/ui-ngx/src/app/shared/components/string-items-list.component.html index b50e0ecce8..d467fbe5f0 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.html +++ b/ui-ngx/src/app/shared/components/string-items-list.component.html @@ -30,7 +30,7 @@ close @@ -49,12 +48,11 @@ - - - - - {{ 'common.not-found' | translate }} - + @for (value of filteredValues | async; track value.value) { + + } @empty { + {{ 'common.not-found' | translate }} + } {{ hint }} diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.ts b/ui-ngx/src/app/shared/components/string-items-list.component.ts index a2fa28d0ff..a4a8ebd49b 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.ts +++ b/ui-ngx/src/app/shared/components/string-items-list.component.ts @@ -30,6 +30,7 @@ import { coerceArray, coerceBoolean } from '@shared/decorators/coercion'; import { Observable, of } from 'rxjs'; import { filter, mergeMap, share, tap } from 'rxjs/operators'; import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { isDefined } from '@core/utils'; export interface StringItemsOption { name: string; @@ -116,6 +117,9 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { @coerceArray() predefinedValues: StringItemsOption[]; + @Input() + fetchOptionsFn: (searchText?: string) => Observable>; + get itemsControl(): AbstractControl { return this.stringItemsForm.get('items'); } @@ -124,7 +128,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { return this.stringItemsForm.get('item'); } - onTouched = () => {}; + private onTouched = () => {}; private propagateChange: (value: any) => void = () => {}; private dirty = false; @@ -136,7 +140,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { } ngOnInit() { - if (this.predefinedValues) { + if (this.predefinedValues || isDefined(this.fetchOptionsFn)) { this.filteredValues = this.itemControl.valueChanges .pipe( tap((value) => { @@ -147,7 +151,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { } }), filter((value) => typeof value === 'string'), - mergeMap(name => this.fetchValues(name)), + mergeMap(name => this.fetchOptionsFn ? this.fetchOptionsFn(name) : this.fetchValues(name)), share() ); } @@ -199,19 +203,16 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { this.dirty = true; } - addItem(event: MatChipInputEvent): void { - const item = event.value?.trim() ?? ''; - if (item) { - if (this.predefinedValues) { - const findItems = this.predefinedValues - .filter(value => value.name.toLowerCase().includes(item.toLowerCase())); - if (findItems.length === 1) { - this.add(findItems[0]); - } - } else { - this.add({value: item, name: item}); - } + addOnBlur(event: FocusEvent) { + const target: HTMLElement = event.relatedTarget as HTMLElement; + if (target && target.tagName !== 'MAT-OPTION') { + this.addItem(this.stringItemInput.nativeElement.value ?? '') } + this.onTouched(); + } + + addOnEnd(event: MatChipInputEvent): void { + this.addItem(event.value ?? '') } removeItems(item: StringItemsOption) { @@ -239,6 +240,21 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { return values ? values.name : undefined; } + private addItem(value: string) { + const item = value.trim(); + if (item) { + if (this.predefinedValues) { + const findItems = this.predefinedValues + .filter(value => value.name.toLowerCase().includes(item.toLowerCase())); + if (findItems.length === 1) { + this.add(findItems[0]); + } + } else { + this.add({value: item, name: item}); + } + } + } + private add(item: StringItemsOption) { if (!this.modelValue || this.modelValue.indexOf(item.value) === -1) { if (!this.modelValue) { From 44f1e139bde66aec55ba0fbace0e078fd84f3ea1 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 2 Apr 2025 13:41:59 +0300 Subject: [PATCH 33/54] UI: Add allowUserValue for string items list --- .../components/string-items-list.component.ts | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.ts b/ui-ngx/src/app/shared/components/string-items-list.component.ts index a4a8ebd49b..5929bbe924 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.ts +++ b/ui-ngx/src/app/shared/components/string-items-list.component.ts @@ -14,7 +14,16 @@ /// limitations under the License. /// -import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { + Component, + DestroyRef, + ElementRef, + forwardRef, + Input, + OnInit, + ViewChild, + ViewEncapsulation +} from '@angular/core'; import { AbstractControl, ControlValueAccessor, @@ -31,6 +40,7 @@ import { Observable, of } from 'rxjs'; import { filter, mergeMap, share, tap } from 'rxjs/operators'; import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; import { isDefined } from '@core/utils'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export interface StringItemsOption { name: string; @@ -120,6 +130,10 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { @Input() fetchOptionsFn: (searchText?: string) => Observable>; + @Input() + @coerceBoolean() + allowUserValue = false; + get itemsControl(): AbstractControl { return this.stringItemsForm.get('items'); } @@ -132,7 +146,8 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { private propagateChange: (value: any) => void = () => {}; private dirty = false; - constructor(private fb: FormBuilder) { + constructor(private fb: FormBuilder, + private destroyRef: DestroyRef) { this.stringItemsForm = this.fb.group({ item: [null], items: [null] @@ -151,6 +166,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { } }), filter((value) => typeof value === 'string'), + tap(name => this.searchText = name), mergeMap(name => this.fetchOptionsFn ? this.fetchOptionsFn(name) : this.fetchValues(name)), share() ); @@ -243,12 +259,20 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { private addItem(value: string) { const item = value.trim(); if (item) { - if (this.predefinedValues) { + if (this.predefinedValues && !this.allowUserValue) { const findItems = this.predefinedValues .filter(value => value.name.toLowerCase().includes(item.toLowerCase())); if (findItems.length === 1) { this.add(findItems[0]); } + } else if (isDefined(this.fetchOptionsFn) && !this.allowUserValue) { + this.fetchOptionsFn(item).pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((findItems) => { + if (findItems.length === 1) { + this.add(findItems[0]); + } + }) } else { this.add({value: item, name: item}); } @@ -272,7 +296,6 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { if (!this.predefinedValues?.length) { return of([]); } - this.searchText = searchText; let result = this.predefinedValues; if (searchText && searchText.length) { result = this.predefinedValues.filter(option => option.name.toLowerCase().includes(searchText.toLowerCase())); From d3e20161c96ff744357deeacdc63ae48edd3c5d6 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 2 Apr 2025 14:08:28 +0300 Subject: [PATCH 34/54] UI: Error icon with tooltip for toggle select --- .../common/map/map-settings.component.html | 8 ++-- .../components/toggle-header.component.html | 11 ++++- .../components/toggle-header.component.scss | 6 +++ .../components/toggle-header.component.ts | 45 ++++++++++++++++--- .../assets/locale/locale.constant-en_US.json | 3 +- 5 files changed, 62 insertions(+), 11 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html index fd3af9feb8..de3a3aa66c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html @@ -41,10 +41,10 @@
- {{ 'widgets.maps.overlays.trips' | translate }} - {{ 'widgets.maps.overlays.markers' | translate }} - {{ 'widgets.maps.overlays.polygons' | translate }} - {{ 'widgets.maps.overlays.circles' | translate }} + {{ 'widgets.maps.overlays.trips' | translate }} + {{ 'widgets.maps.overlays.markers' | translate }} + {{ 'widgets.maps.overlays.polygons' | translate }} + {{ 'widgets.maps.overlays.circles' | translate }} - {{ option.name }} + + {{ option.name }} + + warning + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.scss new file mode 100644 index 0000000000..5c127549db --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.scss @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-shape-fill-image-settings-panel { + width: 700px; + max-width: 90vw; + min-height: 300px; + max-height: 90vh; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-xs} { + width: 90vw; + } + .tb-shape-fill-image-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-form-row { + height: auto; + } + .tb-shape-fill-image-settings-panel-body { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + overflow: auto; + } + .tb-shape-fill-image-settings-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts new file mode 100644 index 0000000000..0d14c5f81c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component.ts @@ -0,0 +1,101 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { WidgetService } from '@core/http/widget.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ShapeFillImageSettings, ShapeFillImageType } from '@shared/models/widget/maps/map.models'; + +@Component({ + selector: 'tb-shape-fill-image-settings-panel', + templateUrl: './shape-fill-image-settings-panel.component.html', + providers: [], + styleUrls: ['./shape-fill-image-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ShapeFillImageSettingsPanelComponent implements OnInit { + + @Input() + shapeFillImageSettings: ShapeFillImageSettings; + + @Output() + shapeFillImageSettingsApplied = new EventEmitter(); + + ShapeFillImageType = ShapeFillImageType; + + shapeFillImageSettingsFormGroup: UntypedFormGroup; + + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + + constructor(private fb: UntypedFormBuilder, + private popover: TbPopoverComponent, + private widgetService: WidgetService, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.shapeFillImageSettingsFormGroup = this.fb.group( + { + type: [this.shapeFillImageSettings?.type || ShapeFillImageType.image, []], + image: [this.shapeFillImageSettings?.image, [Validators.required]], + opacity: [this.shapeFillImageSettings?.opacity, [Validators.min(0), Validators.max(1)]], + angle: [this.shapeFillImageSettings?.angle, [Validators.min(0), Validators.max(360)]], + scale: [this.shapeFillImageSettings?.scale, [Validators.min(0)]], + imageFunction: [this.shapeFillImageSettings?.imageFunction, [Validators.required]], + images: [this.shapeFillImageSettings?.images, []] + } + ); + this.shapeFillImageSettingsFormGroup.get('type').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateValidators(); + setTimeout(() => {this.popover?.updatePosition();}, 0); + }); + this.updateValidators(); + } + + cancel() { + this.popover?.hide(); + } + + applyShapeFillImageSettings() { + const shapeFillImageSettings: ShapeFillImageSettings = this.shapeFillImageSettingsFormGroup.value; + this.shapeFillImageSettingsApplied.emit(shapeFillImageSettings); + this.popover?.hide(); + } + + private updateValidators() { + const type: ShapeFillImageType = this.shapeFillImageSettingsFormGroup.get('type').value; + if (type === ShapeFillImageType.image) { + this.shapeFillImageSettingsFormGroup.get('image').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('opacity').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('angle').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('scale').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('imageFunction').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('images').disable({emitEvent: false}); + } else { + this.shapeFillImageSettingsFormGroup.get('image').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('opacity').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('angle').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('scale').disable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('imageFunction').enable({emitEvent: false}); + this.shapeFillImageSettingsFormGroup.get('images').enable({emitEvent: false}); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html new file mode 100644 index 0000000000..da4feaccf2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.html @@ -0,0 +1,29 @@ + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts new file mode 100644 index 0000000000..5f707abced --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, forwardRef, Input, Renderer2, ViewContainerRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { ShapeFillImageSettings, ShapeFillImageType } from '@shared/models/widget/maps/map.models'; +import { + ShapeFillImageSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component'; + +@Component({ + selector: 'tb-shape-fill-image-settings', + templateUrl: './shape-fill-image-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ShapeFillImageSettingsComponent), + multi: true + } + ] +}) +export class ShapeFillImageSettingsComponent implements ControlValueAccessor { + + @Input() + disabled: boolean; + + ShapeFillImageType = ShapeFillImageType; + + modelValue: ShapeFillImageSettings; + + private propagateChange: (v: any) => void = () => { }; + + constructor(private popoverService: TbPopoverService, + private renderer: Renderer2, + private cd: ChangeDetectorRef, + private viewContainerRef: ViewContainerRef) {} + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: ShapeFillImageSettings): void { + if (value) { + this.modelValue = value; + } + } + + openImageSettingsPopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: ShapeFillImageSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'left', + context: { + shapeFillImageSettings: this.modelValue, + }, + isModal: true + }).tbComponentRef.instance.shapeFillImageSettingsApplied.subscribe((shapeFillImageSettings) => { + this.modelValue = shapeFillImageSettings; + this.propagateChange(this.modelValue); + this.cd.detectChanges(); + }); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.html new file mode 100644 index 0000000000..c4b931dafd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.html @@ -0,0 +1,84 @@ + +
+
widgets.maps.data-layer.shape.stripe-pattern
+
+
+
+
widgets.maps.data-layer.shape.first-stripe
+
+ + + px + + +
+
+
+
widgets.maps.data-layer.shape.second-stripe
+
+ + + px + + +
+
+
+
widgets.maps.data-layer.shape.angle
+ + +
deg
+
+
+
+
+ widgets.background.preview +
+
+
+
+
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.scss new file mode 100644 index 0000000000..5f3af9894b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.scss @@ -0,0 +1,80 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../../../../../../../../../scss/constants'; + +.tb-shape-fill-stripe-settings-panel { + width: 700px; + max-width: 90vw; + min-height: 300px; + max-height: 90vh; + display: flex; + flex-direction: column; + gap: 16px; + @media #{$mat-xs} { + width: 90vw; + } + .tb-shape-fill-stripe-settings-title { + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0.25px; + color: rgba(0, 0, 0, 0.87); + } + .tb-form-row { + height: auto; + } + .tb-shape-fill-stripe-settings-panel-body { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + overflow: auto; + } + .tb-shape-fill-stripe-settings-preview { + flex: 1; + background: #fff; + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.12); + display: flex; + flex-direction: column; + padding: 12px 16px 24px 16px; + align-items: center; + gap: 12px; + } + .tb-shape-fill-stripe-settings-preview-title { + align-self: stretch; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 24px; + color: rgba(0, 0, 0, 0.38); + } + .tb-shape-fill-stripe-settings-preview-box { + position: relative; + width: 136px; + height: 118px; + border-radius: 2.666px; + } + + .tb-shape-fill-stripe-settings-panel-buttons { + height: 40px; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: flex-end; + align-items: flex-end; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts new file mode 100644 index 0000000000..c9df56338b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component.ts @@ -0,0 +1,106 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MapDataLayerType, ShapeFillStripeSettings } from '@shared/models/widget/maps/map.models'; +import { DomSanitizer } from '@angular/platform-browser'; +import { + generateStripePreviewUrl +} from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component'; +import { ComponentStyle } from '@shared/models/widget-settings.models'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; +import { DatasourceType } from '@shared/models/widget.models'; + +@Component({ + selector: 'tb-shape-fill-stripe-settings-panel', + templateUrl: './shape-fill-stripe-settings-panel.component.html', + providers: [], + styleUrls: ['./shape-fill-stripe-settings-panel.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ShapeFillStripeSettingsPanelComponent implements OnInit { + + @Input() + shapeFillStripeSettings: ShapeFillStripeSettings; + + @Input() + context: MapSettingsContext; + + @Input() + dsType: DatasourceType; + + @Input() + dsEntityAliasId: string; + + @Input() + dsDeviceId: string; + + @Input() + dataLayerType: MapDataLayerType; + + @Output() + shapeFillStripeSettingsApplied = new EventEmitter(); + + stripePreviewStyle: ComponentStyle; + + shapeFillStripeSettingsFormGroup: UntypedFormGroup; + + constructor(private fb: UntypedFormBuilder, + private sanitizer: DomSanitizer, + private popover: TbPopoverComponent, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.shapeFillStripeSettingsFormGroup = this.fb.group( + { + weight: [this.shapeFillStripeSettings?.weight, [Validators.min(0)]], + color: [this.shapeFillStripeSettings?.color, []], + spaceWeight: [this.shapeFillStripeSettings?.spaceWeight, [Validators.min(0)]], + spaceColor: [this.shapeFillStripeSettings?.spaceColor, []], + angle: [this.shapeFillStripeSettings?.angle, [Validators.min(0), Validators.max(180)]] + } + ); + this.shapeFillStripeSettingsFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updatePreview(); + }); + this.updatePreview(); + } + + cancel() { + this.popover?.hide(); + } + + applyShapeFillStripeSettings() { + const shapeFillStripeSettings: ShapeFillStripeSettings = this.shapeFillStripeSettingsFormGroup.value; + this.shapeFillStripeSettingsApplied.emit(shapeFillStripeSettings); + this.popover?.hide(); + } + + private updatePreview() { + const shapeFillStripeSettings: ShapeFillStripeSettings = this.shapeFillStripeSettingsFormGroup.value; + const previewUrl = generateStripePreviewUrl(shapeFillStripeSettings); + this.stripePreviewStyle = { + background: this.sanitizer.bypassSecurityTrustStyle(`url(${previewUrl}) no-repeat 50% 50% / cover`) + }; + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.html new file mode 100644 index 0000000000..2188e97ae6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.html @@ -0,0 +1,27 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts new file mode 100644 index 0000000000..63b2b4b212 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component.ts @@ -0,0 +1,148 @@ +/// +/// Copyright © 2016-2025 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { ChangeDetectorRef, Component, forwardRef, Input, Renderer2, ViewContainerRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { MapDataLayerType, ShapeFillStripeSettings } from '@shared/models/widget/maps/map.models'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { isDefinedAndNotNull, stringToBase64 } from '@core/utils'; +import { MapSettingsContext } from '@home/components/widget/lib/settings/common/map/map-settings.component.models'; +import { DatasourceType } from '@shared/models/widget.models'; +import { + ShapeFillStripeSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component'; + +@Component({ + selector: 'tb-shape-fill-stripe-settings', + templateUrl: './shape-fill-stripe-settings.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ShapeFillStripeSettingsComponent), + multi: true + } + ] +}) +export class ShapeFillStripeSettingsComponent implements ControlValueAccessor { + + @Input() + disabled: boolean; + + @Input() + context: MapSettingsContext; + + @Input() + dsType: DatasourceType; + + @Input() + dsEntityAliasId: string; + + @Input() + dsDeviceId: string; + + @Input() + dataLayerType: MapDataLayerType; + + modelValue: ShapeFillStripeSettings; + + stripePreviewUrl: SafeUrl; + + private propagateChange: (v: any) => void = () => { }; + + constructor(private popoverService: TbPopoverService, + private sanitizer: DomSanitizer, + private renderer: Renderer2, + private cd: ChangeDetectorRef, + private viewContainerRef: ViewContainerRef) {} + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: ShapeFillStripeSettings): void { + if (value) { + this.modelValue = value; + } + this.updatePreview(); + } + + openStripeSettingsPopup($event: Event, matButton: MatButton) { + if ($event) { + $event.stopPropagation(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: ShapeFillStripeSettingsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: 'left', + context: { + shapeFillStripeSettings: this.modelValue, + context: this.context, + dsType: this.dsType, + dsEntityAliasId: this.dsEntityAliasId, + dsDeviceId: this.dsDeviceId, + dataLayerType: this.dataLayerType + }, + isModal: true + }).tbComponentRef.instance.shapeFillStripeSettingsApplied.subscribe((shapeFillStripeSettings) => { + this.modelValue = shapeFillStripeSettings; + this.updatePreview(); + this.propagateChange(this.modelValue); + this.cd.detectChanges(); + }); + } + } + + private updatePreview() { + this.stripePreviewUrl = this.sanitizer.bypassSecurityTrustUrl(generateStripePreviewUrl(this.modelValue)); + } +} + +export const generateStripePreviewUrl = (settings: ShapeFillStripeSettings): string => { + const weight = isDefinedAndNotNull(settings?.weight) ? settings.weight : 3; + const spaceWeight = isDefinedAndNotNull(settings?.spaceWeight) ? settings.spaceWeight : 9; + const angle = isDefinedAndNotNull(settings?.angle) ? settings.angle : 45; + const height = weight + spaceWeight; + const color = settings?.color?.color || '#8f8f8f'; + const spaceColor = settings?.spaceColor?.color || 'rgba(143,143,143,0)'; + const svgStr = ` + + + + + + + + `; + const encodedSvg = stringToBase64(svgStr); + return `data:image/svg+xml;base64,${encodedSvg}`; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index c00c816073..233b202684 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts @@ -255,6 +255,18 @@ import { MapDataSourcesComponent } from '@home/components/widget/lib/settings/co import { MapDataSourceRowComponent } from '@home/components/widget/lib/settings/common/map/map-data-source-row.component'; +import { + ShapeFillImageSettingsComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-image-settings.component'; +import { + ShapeFillImageSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-image-settings-panel.component'; +import { + ShapeFillStripeSettingsComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings.component'; +import { + ShapeFillStripeSettingsPanelComponent +} from '@home/components/widget/lib/settings/common/map/shape-fill-stripe-settings-panel.component'; @NgModule({ declarations: [ @@ -342,6 +354,10 @@ import { MarkerImageSettingsComponent, MarkerImageSettingsPanelComponent, MarkerClusteringSettingsComponent, + ShapeFillStripeSettingsComponent, + ShapeFillStripeSettingsPanelComponent, + ShapeFillImageSettingsComponent, + ShapeFillImageSettingsPanelComponent, MapDataLayerDialogComponent, MapDataLayerRowComponent, MapDataLayersComponent, diff --git a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts index bac8c8fb75..e09d5e5756 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts @@ -487,8 +487,40 @@ export const defaultBaseTripsDataLayerSettings = (mapType: MapType): Partial => mergeDeep({ + fillType: ShapeFillType.color, fillColor: { type: DataLayerColorType.constant, color: 'rgba(51,136,255,0.2)', }, + fillImage: { + type: ShapeFillImageType.image, + image: '/assets/widget-preview-empty.svg', + opacity: 1, + angle: 0, + scale: 1 + }, + fillStripe: { + weight: 3, + color: { + type: DataLayerColorType.constant, + color: '#8f8f8f' + }, + spaceWeight: 9, + spaceColor: { + type: DataLayerColorType.constant, + color: 'rgba(143,143,143,0)', + }, + angle: 45 + }, strokeColor: { type: DataLayerColorType.constant, color: '#3388ff', @@ -539,10 +592,31 @@ export const defaultCirclesDataLayerSettings = (mapType: MapType, functionsOnly } as CirclesDataLayerSettings, defaultBaseCirclesDataLayerSettings(mapType) as CirclesDataLayerSettings); export const defaultBaseCirclesDataLayerSettings = (mapType: MapType): Partial => mergeDeep({ + fillType: ShapeFillType.color, fillColor: { type: DataLayerColorType.constant, color: 'rgba(51,136,255,0.2)', }, + fillImage: { + type: ShapeFillImageType.image, + image: '/assets/widget-preview-empty.svg', + opacity: 1, + angle: 0, + scale: 1 + }, + fillStripe: { + weight: 3, + color: { + type: DataLayerColorType.constant, + color: '#8f8f8f' + }, + spaceWeight: 9, + spaceColor: { + type: DataLayerColorType.constant, + color: 'rgba(143,143,143,0)', + }, + angle: 45 + }, strokeColor: { type: DataLayerColorType.constant, color: '#3388ff', @@ -596,10 +670,8 @@ export const mapDataSourceValid = (dataSource: MapDataSourceSettings): boolean = if (dataSource.dsType === DatasourceType.device && !dataSource.dsDeviceId) { return false; } - if (dataSource.dsType === DatasourceType.entity && !dataSource.dsEntityAliasId) { - return false; - } - return true; + return !(dataSource.dsType === DatasourceType.entity && !dataSource.dsEntityAliasId); + }; export const mapDataSourceValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { @@ -1112,6 +1184,13 @@ export interface MarkerIconInfo { size: [number, number]; } +export interface ShapeFillImageInfo { + url: string; + opacity?: number; + angle?: number; + scale?: number; +} + export type MapStringFunction = (data: FormattedData, dsData: FormattedData[]) => string; @@ -1126,6 +1205,9 @@ export type ClusterMarkerColorFunction = (data: FormattedData[] export type MarkerPositionFunction = (origXPos: number, origYPos: number, data: FormattedData, dsData: FormattedData[], aspect: number) => { x: number, y: number }; +export type ShapeFillImageFunction = (data: FormattedData, images: string[], + dsData: FormattedData[]) => ShapeFillImageInfo; + export type TbPolygonRawCoordinate = L.LatLngTuple | L.LatLngTuple[] | L.LatLngTuple[][]; export type TbPolygonRawCoordinates = TbPolygonRawCoordinate[]; export type TbPolyData = L.LatLngTuple[] | L.LatLngTuple[][] | L.LatLngTuple[][][]; @@ -1271,11 +1353,13 @@ const imageLoader = (imageUrl: string): Observable => new Obse image.src = imageUrl; }); -const loadImageAspect = (imageUrl: string): Observable => - imageLoader(imageUrl).pipe(map(image => image.width / image.height)); +const loadImageSize = (imageUrl: string): Observable<[number, number]> => + imageLoader(imageUrl).pipe(map(image => [image.width, image.height])); export interface ImageWithAspect { url: string; + width: number; + height: number; aspect: number; } @@ -1289,9 +1373,14 @@ export const loadImageWithAspect = (imagePipe: ImagePipe, imageUrl: string): Obs return imagePipe.transform(imageUrl, {asString: true, ignoreLoadingImage: true}).pipe( switchMap((res) => { const url = res as string; - return loadImageAspect(url).pipe( - map((aspect) => { - imageWithAspect = {url, aspect}; + return loadImageSize(url).pipe( + map((size) => { + imageWithAspect = { + url, + width: size[0], + height: size[1], + aspect: size[0]/size[1] + }; imageAspectMap[hash] = imageWithAspect; return imageWithAspect; }) diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn.md new file mode 100644 index 0000000000..b13d612c8e --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn.md @@ -0,0 +1,52 @@ +#### Shape fill image function + +
+
+ +*function (data, images, dsData): {url: string}* + +A JavaScript function used to compute shape fill image. + +**Parameters:** + +
    + {% include widget/lib/map/shape_fill_image_fn_args %} +
+ +**Returns:** + +Should return shape fill image data having the following structure: + +```typescript +{ + url: string; + opacity?: number; + angle?: number; + scale?: number; +} +``` + +- *url* - fill image url; +- *opacity* - optional image opacity, number value from 0 to 1; +- *angle* - optional image rotation angle, number value from 0 to 360; +- *scale* - optional image scale, number value (1 - original size, smaller value - scale down, bigger value - scale up); + +In case no data is returned, default fill image will be used. + +
+ +##### Examples + +
    +
  • +TODO: +
  • +
+ +```javascript +TODO: +{:copy-code} +``` + +
+
diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn_args.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn_args.md new file mode 100644 index 0000000000..07b60f72ae --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/shape_fill_image_fn_args.md @@ -0,0 +1,9 @@ +
  • data: FormattedData object associated with data layer (markers/polygons/circles) or data point of the route (trips data layer).
    + Represents basic entity properties (ex. entityId, entityName)
    and provides access to other entity attributes/timeseries declared in datasource of the data layer configuration. +
  • +
  • images: string[] - array of image urls configured in the Shape fill images section. +
  • +
  • dsData: FormattedData[] - All available data associated with data layers including additional datasources as array of FormattedData objects
    + resolved from configured datasources. Each object represents basic entity properties (ex. entityId, entityName)
    + and provides access to other entity attributes/timeseries declared in datasources of data layers configuration including additional datasources of the map configuration. +
  • 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 ec047f91ae..e4f4721f3c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -8011,8 +8011,6 @@ "groups": "Groups", "groups-hint": "List of group names assigned to the overlay, used to toggle its visibility on the map.", "color": "Color", - "fill-color": "Fill color", - "stroke": "Stroke", "color-settings": "Color settings", "color-type-constant": "Constant", "color-type-range": "Range", @@ -8127,6 +8125,27 @@ "points": "Points", "point-tooltip": "Point tooltip" }, + "shape": { + "fill": "Fill", + "fill-type-color": "Color", + "fill-type-stripe": "Stripe", + "fill-type-image": "Image", + "color": "Color", + "stripe": "Stripe", + "image": "Image", + "stroke": "Stroke", + "fill-image": "Fill image", + "fill-image-type-image": "Image", + "fill-image-type-function": "Function", + "opacity": "Opacity", + "angle": "Rotation angle", + "scale": "Scale", + "fill-image-function": "Shape fill image function", + "fill-images": "Shape fill images", + "stripe-pattern": "Stripe pattern", + "first-stripe": "First stripe", + "second-stripe": "Second stripe" + }, "polygon": { "polygon-key": "Polygon key", "polygon-key-required": "Polygon key required", diff --git a/ui-ngx/src/typings/leaflet-extend-tb.d.ts b/ui-ngx/src/typings/leaflet-extend-tb.d.ts index 1ea249b478..3b982a527e 100644 --- a/ui-ngx/src/typings/leaflet-extend-tb.d.ts +++ b/ui-ngx/src/typings/leaflet-extend-tb.d.ts @@ -25,6 +25,18 @@ declare module 'leaflet' { interface MarkerOptions { tbMarkerData?: FormattedData; } + interface Map { + _patterns: {[id: number]: L.TB.Pattern}; + _defRoot: SVGDefsElement; + addPattern(pattern: L.TB.Pattern): Map; + removePattern(pattern: L.TB.Pattern): Map; + hasPattern(pattern: L.TB.Pattern): boolean; + _initDefRoot(): void; + } + + interface PathOptions { + fillPattern?: L.TB.Pattern | undefined; + } interface TileLayer { _url: string; @@ -152,6 +164,109 @@ declare module 'leaflet' { container: HTMLElement; } + interface PathOptions { + fillPattern?: Pattern | undefined; + } + + interface PatternOptions { + x?: number | undefined; + y?: number | undefined; + width?: number | undefined; + height?: number | undefined; + patternUnits?: "userSpaceOnUse" | "objectBoundingBox" | undefined; + patternContentUnits?: "userSpaceOnUse" | "objectBoundingBox" | undefined; + patternTransform?: string | null | undefined; + preserveAspectRatioAlign?: "none" | "xMinYMin" | "xMidYMin" | "xMaxYMin" | "xMinYMid" | "xMidYMid" | "xMaxYMid" | "xMinYMax" | "xMidYMax" | "xMaxYMax" | undefined; + preserveAspectRatioMeetOrSlice?: "meet" | "slice" | undefined; + viewBox?: [number, number, number, number] | undefined; + angle?: number | null | undefined; + className?: string | undefined; + } + + interface PatternElementOptions { + className?: string | undefined; + } + + interface PatternShapeOptions extends PatternElementOptions { + stroke?: boolean | undefined; + color?: string | undefined; + weight?: number | undefined; + opacity?: number | undefined; + lineCap?: "butt" | "round" | "square" | "inherit" | undefined; + lineJoin?: "butt" | "round" | "square" | "inherit" | undefined; + dashArray?: number[] | null | undefined; + dashOffset?: number | null | undefined; + fill?: boolean | undefined; + fillColor?: string | undefined; + fillOpacity?: number | undefined; + fillRule?: "nonzero" | "evenodd" | "inherit" | undefined; + fillPattern?: Pattern | null | undefined; + pointerEvents?: string | undefined; + interactive?: boolean | undefined; + } + + interface PatternRectOptions extends PatternShapeOptions { + x?: number | undefined; + y?: number | undefined; + width?: number | undefined; + height?: number | undefined; + rx?: number | null | undefined; + ry?: number | null | undefined; + } + + interface PatternPathOptions extends PatternShapeOptions { + d?: string | null | undefined; + } + + interface PatternImageOptions extends PatternElementOptions { + imageUrl: string; + width: number; + height: number; + opacity?: number; + angle?: number; + scale?: number; + } + + class Pattern extends L.Evented { + constructor(options?: PatternOptions); + onAdd(map: L.Map): void; + onRemove(map: L.Map): void; + redraw(): this; + setStyle(style: PatternOptions): this; + addTo(map: L.Map): this; + remove(): this; + removeFrom(map: L.Map): this; + addElement(element: PatternElement): PatternElement | undefined; + } + + abstract class PatternElement extends L.Class { + protected constructor(options?: PatternElementOptions); + onAdd(pattern: Pattern): void; + addTo(pattern: Pattern): this; + redraw(): this; + setStyle(style: PatternElementOptions): this; + } + + abstract class PatternShape extends PatternElement { + protected constructor(options?: PatternShapeOptions); + setStyle(style: PatternShapeOptions): this; + } + + class PatternRect extends PatternShape { + constructor(options?: PatternRectOptions); + setStyle(style: PatternRectOptions): this; + } + + class PatternPath extends PatternShape { + constructor(options?: PatternPathOptions); + setStyle(style: PatternPathOptions): this; + } + + class PatternImage extends PatternElement { + constructor(options: PatternImageOptions); + setStyle(style: PatternImageOptions): this; + } + function sidebar(options: SidebarControlOptions): SidebarControl; function sidebarPane(options: O): SidebarPaneControl; diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 602839b2da..6e86f3c2b0 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -1581,10 +1581,10 @@ dependencies: tslib "^2.3.0" -"@geoman-io/leaflet-geoman-free@2.17.0": - version "2.17.0" - resolved "https://registry.yarnpkg.com/@geoman-io/leaflet-geoman-free/-/leaflet-geoman-free-2.17.0.tgz#9c8fce5c7a85e5d7ece3a7e5d0b82dbf2329bd81" - integrity sha512-vAY9tKB2I/Ui8d3QUBuebWnunI2sGjsfAUTXMMcf5UpISvPz67io4hpbKXid9GNsW6P4LGv1+ZzrmkpM78GzHA== +"@geoman-io/leaflet-geoman-free@2.18.3": + version "2.18.3" + resolved "https://registry.yarnpkg.com/@geoman-io/leaflet-geoman-free/-/leaflet-geoman-free-2.18.3.tgz#a41489920b175931fba2a1e8e81347f9e3be5481" + integrity sha512-XzxSKRk2UJUVeGiOt1jU2hyo412Qee1Q0Xsfw4A2r8EoUIo48XKSWfusYe7E53fSPr0aYgZxPevnFdcUXimpdA== dependencies: "@turf/boolean-contains" "^6.5.0" "@turf/kinks" "^6.5.0" From 6aff579f0321b3484f010d7db8da29e40c6ff4fe Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 8 Apr 2025 11:19:17 +0300 Subject: [PATCH 53/54] UI: New Maps - fix marker drag in clustering mode. --- .../lib/maps/data-layer/markers-data-layer.ts | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts index 093cc265bb..1ca5e0b1bf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/markers-data-layer.ts @@ -396,6 +396,8 @@ class TbMarkerDataLayerItem extends TbLatestDataLayerItem void; + private dragEnd: () => void; constructor(data: FormattedData, dsData: FormattedData[], @@ -415,6 +417,8 @@ class TbMarkerDataLayerItem extends TbLatestDataLayerItem, dsData: FormattedData[]): L.Marker { this.iconClassList = []; const location = this.dataLayer.dataProcessor.extractLocation(data, dsData); + this.dragStart = this._dragStart.bind(this); + this.dragEnd = this._dragEnd.bind(this); this.marker = L.marker(location, { tbMarkerData: data, snapIgnore: !this.dataLayer.isSnappable(), @@ -465,44 +469,28 @@ class TbMarkerDataLayerItem extends TbLatestDataLayerItem { - this.moving = true; - }); - this.marker.on('dragend', () => { - this.saveMarkerLocation(); - this.moving = false; - }); - } else { + this.marker.options.draggable = true; + this.marker.dragging?.enable(); + if (!this.settings.markerClustering?.enable) { this.marker.pm.setOptions({ snappable: this.dataLayer.isSnappable() }); this.marker.pm.enableLayerDrag(); - this.marker.on('pm:dragstart', () => { - (this.marker.dragging as any)._draggable = { _moved: true, off: (_args: any) => { return { disable: () => {}} } }; - (this.marker.dragging as any)._enabled = true; - this.moving = true; - }); - this.marker.on('pm:dragend', () => { - this.saveMarkerLocation(); - delete (this.marker.dragging as any)._draggable; - delete (this.marker.dragging as any)._enabled; - this.moving = false; - }); } + const evtPrefix = this.settings.markerClustering?.enable ? '' : 'pm:'; + this.marker.on(evtPrefix + 'dragstart', this.dragStart); + this.marker.on(evtPrefix + 'dragend', this.dragEnd); } protected disableDrag(): void { - if (this.settings.markerClustering?.enable) { - this.marker.options.draggable = false; - this.marker.off('dragstart'); - this.marker.off('dragend'); - } else { + this.marker.options.draggable = false; + this.marker.dragging?.disable(); + if (!this.settings.markerClustering?.enable) { this.marker.pm.disableLayerDrag(); - this.marker.off('pm:dragstart'); - this.marker.off('pm:dragend'); } + const evtPrefix = this.settings.markerClustering?.enable ? '' : 'pm:'; + this.marker.off(evtPrefix + 'dragstart', this.dragStart); + this.marker.off(evtPrefix + 'dragend', this.dragEnd); } protected removeDataItemTitle(): string { @@ -513,6 +501,15 @@ class TbMarkerDataLayerItem extends TbLatestDataLayerItem Date: Tue, 8 Apr 2025 13:43:04 +0300 Subject: [PATCH 54/54] UI: Improve entity data subscription - cancel uncompleted subscribe operation when unsubscribe is called. --- .../app/core/api/entity-data-subscription.ts | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 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 6579b952af..3adda4cd27 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -64,7 +64,7 @@ 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 { firstValueFrom, from, Observable, of, ReplaySubject, Subject, tap } from 'rxjs'; +import { firstValueFrom, from, Observable, of, ReplaySubject, Subject, Subscription } from 'rxjs'; import { EntityId } from '@shared/models/id/entity-id'; import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; import { @@ -75,6 +75,7 @@ import { } from '@shared/models/js-function.models'; import { HttpClient } from '@angular/common/http'; import Timeout = NodeJS.Timeout; +import { finalize, switchMap } from 'rxjs/operators'; declare type DataKeyFunction = (time: number, prevValue: any) => any; declare type DataKeyPostFunction = (time: number, value: any, prevValue: any, timePrev: number, prevOrigValue: any) => any; @@ -141,6 +142,7 @@ export class EntityDataSubscription { private aggTsValues: Array; private aggTsComparisonValues: Array; + private subscribeSubscription: Subscription; private entityDataResolveSubject: Subject; private pageData: PageData; private prematureUpdates: Array>; @@ -216,7 +218,7 @@ export class EntityDataSubscription { if (isNotEmptyTbFunction(dataKey.postFuncBody) && !dataKey.postFunc) { try { dataKey.postFunc = await firstValueFrom(compileTbFunction(this.http, dataKey.postFuncBody, 'time', 'value', 'prevValue', 'timePrev', 'prevOrigValue')); - } catch (e) {} + } catch (e) {/**/} } } let key: string; @@ -245,6 +247,10 @@ export class EntityDataSubscription { } public unsubscribe() { + if (this.subscribeSubscription) { + this.subscribeSubscription.unsubscribe(); + this.subscribeSubscription = null; + } if (this.timeseriesTimer) { clearTimeout(this.timeseriesTimer); this.timeseriesTimer = null; @@ -277,8 +283,14 @@ export class EntityDataSubscription { public subscribe(): Observable { this.entityDataResolveSubject = new ReplaySubject(1); - from(this.initializeSubscription()).pipe( - tap(() => { + const subscribeSubject = new ReplaySubject(1); + this.subscribeSubscription = from(this.initializeSubscription()).pipe( + finalize(() => { + subscribeSubject.next(); + subscribeSubject.complete(); + }) + ).subscribe( + () => { if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { this.started = true; this.dataResolved = true; @@ -621,13 +633,17 @@ export class EntityDataSubscription { ); this.subscriber.subscribe(); } + } + ); + return subscribeSubject.pipe( + switchMap(() => { + if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { + return of(null); + } else { + return this.entityDataResolveSubject.asObservable(); + } }) - ).subscribe(); - if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { - return of(null); - } else { - return this.entityDataResolveSubject.asObservable(); - } + ); } public start() {